├── .codacy.yaml ├── .editorconfig ├── .github └── workflows │ ├── docs.yaml │ └── maven.yml ├── .gitignore ├── .idea └── externalDependencies.xml ├── .java-version ├── .mvn ├── extensions.xml ├── jvm.config └── maven.config ├── LICENSE.txt ├── Makefile ├── README.md ├── RELEASE.md ├── detekt.yml ├── docs ├── AsyncIO.md ├── PromptTemplates.md └── kotlin-notebook-1.png ├── langchain4j-kotlin ├── notebooks │ └── lc4kNotebook.ipynb ├── pom.xml └── src │ ├── main │ ├── kotlin │ │ └── me │ │ │ └── kpavlov │ │ │ └── langchain4j │ │ │ └── kotlin │ │ │ ├── Configuration.kt │ │ │ ├── TypeAliases.kt │ │ │ ├── data │ │ │ └── document │ │ │ │ ├── DocumentLoaderExtensions.kt │ │ │ │ └── DocumentParserExtensions.kt │ │ │ ├── internal │ │ │ └── Logging.kt │ │ │ ├── model │ │ │ ├── adapters │ │ │ │ ├── TokenStreamToReplyFlowAdapter.kt │ │ │ │ └── TokenStreamToStringFlowAdapter.kt │ │ │ └── chat │ │ │ │ ├── ChatModelExtensions.kt │ │ │ │ ├── StreamingChatModelExtensions.kt │ │ │ │ └── request │ │ │ │ └── ChatRequestExtensions.kt │ │ │ ├── prompt │ │ │ ├── ClasspathPromptTemplateSource.kt │ │ │ ├── PromptTemplate.kt │ │ │ ├── PromptTemplateFactory.kt │ │ │ ├── PromptTemplateSource.kt │ │ │ ├── RenderablePromptTemplate.kt │ │ │ ├── SimpleTemplateRenderer.kt │ │ │ └── TemplateRenderer.kt │ │ │ ├── rag │ │ │ └── RetrievalAugmentorExtensions.kt │ │ │ └── service │ │ │ ├── AiServiceOrchestrator.kt │ │ │ ├── AiServicesExtensions.kt │ │ │ ├── AsyncAiServices.kt │ │ │ ├── AsyncAiServicesFactory.kt │ │ │ ├── ReflectionHelper.kt │ │ │ ├── ReflectionVariableResolver.kt │ │ │ ├── SystemMessageProvider.kt │ │ │ ├── TemplateSystemMessageProvider.kt │ │ │ ├── TokenStreamExtensions.kt │ │ │ ├── invoker │ │ │ ├── AiServiceOrchestrator.kt │ │ │ ├── HybridVirtualThreadInvocationHandler.kt │ │ │ ├── KServices.kt │ │ │ └── ServiceClassValidator.kt │ │ │ └── memory │ │ │ └── ChatMemoryServiceExtensions.kt │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ ├── dev.langchain4j.spi.prompt.PromptTemplateFactory │ │ │ ├── dev.langchain4j.spi.services.AiServicesFactory │ │ │ └── dev.langchain4j.spi.services.TokenStreamAdapter │ │ └── langchain4j-kotlin.properties │ └── test │ ├── kotlin │ └── me │ │ └── kpavlov │ │ └── langchain4j │ │ └── kotlin │ │ ├── ChatModelTest.kt │ │ ├── Documents.kt │ │ ├── TestEnvironment.kt │ │ ├── adapters │ │ └── ServiceWithFlowTest.kt │ │ ├── data │ │ └── document │ │ │ ├── AsyncDocumentLoaderTest.kt │ │ │ └── DocumentParserExtensionsKtTest.kt │ │ ├── model │ │ └── chat │ │ │ ├── ChatModelExtensionsKtTest.kt │ │ │ ├── ChatModelIT.kt │ │ │ ├── StreamingChatModelExtensionsKtTest.kt │ │ │ ├── StreamingChatModelIT.kt │ │ │ ├── TestSetup.kt │ │ │ └── request │ │ │ └── ChatRequestExtensionsTest.kt │ │ ├── prompt │ │ └── SimpleTemplateRendererTest.kt │ │ └── service │ │ ├── AsyncAiServicesTest.kt │ │ ├── ServiceWithPromptTemplatesTest.kt │ │ ├── ServiceWithSystemMessageProviderTest.kt │ │ ├── TemplateSystemMessageProviderTest.kt │ │ └── invoker │ │ ├── HybridVirtualThreadInvocationHandlerTest.kt │ │ └── KServicesTest.kt │ └── resources │ ├── data │ ├── books │ │ └── captain-blood.txt │ └── notes │ │ ├── blumblefang.txt │ │ └── quantum-computing.txt │ ├── prompts │ └── ServiceWithTemplatesTest │ │ ├── default-system-prompt.mustache │ │ └── default-user-prompt.mustache │ └── simplelogger.properties ├── pom.xml ├── renovate.json5 ├── reports └── pom.xml └── samples ├── pom.xml └── src ├── main ├── java │ └── me │ │ └── kpavlov │ │ └── langchain4j │ │ └── kotlin │ │ └── samples │ │ └── ServiceWithTemplateSourceJavaExample.java ├── kotlin │ └── me │ │ └── kpavlov │ │ └── langchain4j │ │ └── kotlin │ │ └── samples │ │ ├── AsyncAiServiceExample.kt │ │ ├── ChatModelExample.kt │ │ ├── Environment.kt │ │ └── OpenAiChatModelExample.kt └── resources │ ├── prompts │ └── ServiceWithTemplateSourceJavaExample │ │ ├── default-system-prompt.mustache │ │ └── default-user-prompt.mustache │ └── simplelogger.properties └── test └── java └── me └── kpavlov └── langchain4j └── kotlin └── samples └── OpenAiChatModelTest.kt /.codacy.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | documentation: 4 | enabled: true 5 | exclude_paths: 6 | - '**/test/**' -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [*.xml] 23 | indent_size = 4 24 | 25 | [*.kt] 26 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 27 | indent_size = 4 28 | max_line_length = 100 29 | ij_kotlin_name_count_to_use_star_import = 999 30 | ij_kotlin_name_count_to_use_star_import_for_members = 999 31 | # noinspection EditorConfigKeyCorrectness 32 | ktlint_function_naming_ignore_when_annotated_with = "Test" 33 | ij_kotlin_packages_to_use_import_on_demand = unset 34 | 35 | [*.mustache] 36 | insert_final_newline = false 37 | -------------------------------------------------------------------------------- /.github/workflows/docs.yaml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docs to GitHub Pages 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | push: 8 | branches: [ "main" ] 9 | # Allow running this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | jobs: 13 | deploy: 14 | 15 | permissions: 16 | pages: write # to deploy to Pages 17 | id-token: write # to verify the deployment originates from an appropriate source 18 | 19 | environment: 20 | name: github-pages 21 | url: ${{ steps.deployment.outputs.page_url }} 22 | 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Set up JDK 24 27 | uses: actions/setup-java@v4 28 | with: 29 | java-version: '24' 30 | distribution: 'temurin' 31 | cache: maven 32 | 33 | - name: Generate Dokka Site 34 | run: |- 35 | mvn dokka:dokka -pl !reports && \ 36 | mkdir -p target/docs/ && \ 37 | cp -R langchain4j-kotlin/target/dokka target/docs/api 38 | 39 | - name: Upload artifact 40 | uses: actions/upload-pages-artifact@v3 41 | with: 42 | path: target/docs/ 43 | - name: Deploy to GitHub Pages 44 | id: deployment 45 | uses: actions/deploy-pages@v4 46 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Kotlin CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Set up JDK 24 25 | uses: actions/setup-java@v4 26 | with: 27 | java-version: '24' 28 | distribution: 'temurin' 29 | cache: maven 30 | 31 | - name: Build with Maven 32 | run: |- 33 | VERSION=$(mvn -B help:evaluate -Dexpression=project.version -q -DforceStdout) 34 | echo "Project version: $VERSION" 35 | 36 | mvn -B install verify site && 37 | (cd samples && mvn -B clean test -Dlangchain4j-kotlin.version=$VERSION) 38 | 39 | - name: Publish Test Report 40 | uses: mikepenz/action-junit-report@v5 41 | if: success() || failure() # always run even if the previous step fails 42 | with: 43 | report_paths: '**/*-reports/TEST-*.xml' 44 | annotate_only: true 45 | 46 | - name: Run codacy-coverage-reporter 47 | uses: codacy/codacy-coverage-reporter-action@v1.3.0 48 | with: 49 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 50 | coverage-reports: "reports/target/site/kover/report.xml" 51 | # or a comma-separated list for multiple reports 52 | # coverage-reports: , 53 | 54 | - name: Upload coverage reports to Codecov 55 | uses: codecov/codecov-action@v5 56 | with: 57 | token: ${{ secrets.CODECOV_TOKEN }} 58 | slug: kpavlov/langchain4j-kotlin 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | replay_pid* 25 | 26 | ### Maven template 27 | target/ 28 | pom.xml.tag 29 | pom.xml.releaseBackup 30 | pom.xml.versionsBackup 31 | pom.xml.next 32 | release.properties 33 | dependency-reduced-pom.xml 34 | buildNumber.properties 35 | .mvn/timing.properties 36 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 37 | .mvn/wrapper/maven-wrapper.jar 38 | 39 | # Eclipse m2e generated files 40 | # Eclipse Core 41 | .project 42 | # JDT-specific (Eclipse Java Development Tools) 43 | .classpath 44 | 45 | ### JetBrains template 46 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 47 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 48 | 49 | # User-specific stuff 50 | .idea/**/workspace.xml 51 | .idea/**/tasks.xml 52 | .idea/**/usage.statistics.xml 53 | .idea/**/dictionaries 54 | .idea/**/shelf 55 | 56 | # AWS User-specific 57 | .idea/**/aws.xml 58 | 59 | # Generated files 60 | .idea/**/contentModel.xml 61 | 62 | # Sensitive or high-churn files 63 | .idea/**/dataSources/ 64 | .idea/**/dataSources.ids 65 | .idea/**/dataSources.local.xml 66 | .idea/**/sqlDataSources.xml 67 | .idea/**/dynamic.xml 68 | .idea/**/uiDesigner.xml 69 | .idea/**/dbnavigator.xml 70 | 71 | # Gradle 72 | .idea/**/gradle.xml 73 | .idea/**/libraries 74 | 75 | # Gradle and Maven with auto-import 76 | # When using Gradle or Maven with auto-import, you should exclude module files, 77 | # since they will be recreated, and may cause churn. Uncomment if using 78 | # auto-import. 79 | # .idea/artifacts 80 | # .idea/compiler.xml 81 | # .idea/jarRepositories.xml 82 | # .idea/modules.xml 83 | # .idea/*.iml 84 | # .idea/modules 85 | # *.iml 86 | # *.ipr 87 | 88 | # CMake 89 | cmake-build-*/ 90 | 91 | # Mongo Explorer plugin 92 | .idea/**/mongoSettings.xml 93 | 94 | # File-based project format 95 | *.iws 96 | 97 | # IntelliJ 98 | out/ 99 | 100 | # mpeltonen/sbt-idea plugin 101 | .idea_modules/ 102 | 103 | # JIRA plugin 104 | atlassian-ide-plugin.xml 105 | 106 | # Cursive Clojure plugin 107 | .idea/replstate.xml 108 | 109 | # SonarLint plugin 110 | .idea/sonarlint/ 111 | 112 | # Crashlytics plugin (for Android Studio and IntelliJ) 113 | com_crashlytics_export_strings.xml 114 | crashlytics.properties 115 | crashlytics-build.properties 116 | fabric.properties 117 | 118 | # Editor-based Rest Client 119 | .idea/httpRequests 120 | 121 | # Android studio 3.1+ serialized cache file 122 | .idea/caches/build_file_checksums.ser 123 | 124 | ### dotenv template 125 | .env 126 | 127 | ### macOS template 128 | # General 129 | .DS_Store 130 | .AppleDouble 131 | .LSOverride 132 | 133 | # Icon must end with two \r 134 | Icon 135 | 136 | # Thumbnails 137 | ._* 138 | 139 | # Files that might appear in the root of a volume 140 | .DocumentRevisions-V100 141 | .fseventsd 142 | .Spotlight-V100 143 | .TemporaryItems 144 | .Trashes 145 | .VolumeIcon.icns 146 | .com.apple.timemachine.donotpresent 147 | 148 | # Directories potentially created on remote AFP share 149 | .AppleDB 150 | .AppleDesktop 151 | Network Trash Folder 152 | Temporary Items 153 | .apdisk 154 | 155 | ### Linux template 156 | *~ 157 | 158 | # temporary files which can be created if a process still has a handle open of a deleted file 159 | .fuse_hidden* 160 | 161 | # KDE directory preferences 162 | .directory 163 | 164 | # Linux trash folder which might appear on any partition or disk 165 | .Trash-* 166 | 167 | # .nfs files are created when an open file is removed but is still being accessed 168 | .nfs* 169 | 170 | ### Windows template 171 | # Windows thumbnail cache files 172 | Thumbs.db 173 | Thumbs.db:encryptable 174 | ehthumbs.db 175 | ehthumbs_vista.db 176 | 177 | # Dump file 178 | *.stackdump 179 | 180 | # Folder config file 181 | [Dd]esktop.ini 182 | 183 | # Recycle Bin used on file shares 184 | $RECYCLE.BIN/ 185 | 186 | # Windows Installer files 187 | *.cab 188 | *.msi 189 | *.msix 190 | *.msm 191 | *.msp 192 | 193 | # Windows shortcuts 194 | *.lnk 195 | 196 | -------------------------------------------------------------------------------- /.idea/externalDependencies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 21 2 | -------------------------------------------------------------------------------- /.mvn/extensions.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | kr.motd.maven 5 | os-maven-plugin 6 | 1.7.1 7 | 8 | -------------------------------------------------------------------------------- /.mvn/jvm.config: -------------------------------------------------------------------------------- 1 | --add-modules jdk.incubator.vector 2 | --add-opens=java.base/java.io=ALL-UNNAMED 3 | --add-opens=java.base/java.lang=ALL-UNNAMED 4 | --enable-native-access=ALL-UNNAMED 5 | -------------------------------------------------------------------------------- /.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Dkotlin.compiler.incremental=true 2 | -T12C 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | Copyright 2024 Konstantin Pavlov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | mvn --version 3 | mvn clean verify site -Prelease -Dgpg.skip 4 | 5 | apidocs: 6 | mvn clean dokka:dokka -pl !reports && \ 7 | mkdir -p target/docs && \ 8 | cp -R langchain4j-kotlin/target/dokka target/docs/api 9 | 10 | lint:prepare 11 | ktlint && \ 12 | mvn spotless:check detekt:check 13 | 14 | # https://docs.openrewrite.org/recipes/maven/bestpractices 15 | format:prepare 16 | ktlint --format && \ 17 | mvn spotless:apply && \ 18 | mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \ 19 | -Drewrite.activeRecipes=org.openrewrite.maven.BestPractices \ 20 | -Drewrite.exportDatatables=true 21 | 22 | prepare: 23 | @if ! command -v ktlint &> /dev/null; then brew install ktlint --quiet; fi 24 | 25 | all: format lint build 26 | 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LangChain4j-Kotlin 2 | 3 | [![Maven Central](https://img.shields.io/maven-central/v/me.kpavlov.langchain4j.kotlin/langchain4j-kotlin)](https://repo1.maven.org/maven2/me/kpavlov/langchain4j/kotlin/langchain4j-kotlin/) 4 | [![Kotlin CI with Maven](https://github.com/kpavlov/langchain4j-kotlin/actions/workflows/maven.yml/badge.svg?branch=main)](https://github.com/kpavlov/langchain4j-kotlin/actions/workflows/maven.yml) 5 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/644f664ad05a4a009b299bc24c8be4b8)](https://app.codacy.com/gh/kpavlov/langchain4j-kotlin/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 6 | [![Codacy Coverage](https://app.codacy.com/project/badge/Coverage/644f664ad05a4a009b299bc24c8be4b8)](https://app.codacy.com/gh/kpavlov/langchain4j-kotlin/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage) 7 | [![codecov](https://codecov.io/gh/kpavlov/langchain4j-kotlin/graph/badge.svg?token=VYIJ92CYHD)](https://codecov.io/gh/kpavlov/langchain4j-kotlin) 8 | [![Maintainability](https://api.codeclimate.com/v1/badges/176ba2c4e657d3e7981a/maintainability)](https://codeclimate.com/github/kpavlov/langchain4j-kotlin/maintainability) 9 | [![Api Docs](https://img.shields.io/badge/api-docs-blue)](https://kpavlov.github.io/langchain4j-kotlin/api/) 10 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/kpavlov/langchain4j-kotlin) 11 | 12 | Kotlin enhancements for [LangChain4j](https://github.com/langchain4j/langchain4j), providing coroutine support and Flow-based streaming capabilities for chat language models. 13 | 14 | See the [discussion](https://github.com/langchain4j/langchain4j/discussions/1897) on LangChain4j project. 15 | 16 | > ℹ️ This project is a playground for [LangChain4j's Kotlin API](https://docs.langchain4j.dev/tutorials/kotlin). If 17 | > accepted, some code might be adopted into the original [LangChain4j](https://github.com/langchain4j) project and removed 18 | > from here. Mean while, enjoy it here. 19 | 20 | ## Features 21 | 22 | - ✨ [Kotlin Coroutine](https://kotlinlang.org/docs/coroutines-guide.html) support for [ChatLanguageModels](https://docs.langchain4j.dev/tutorials/chat-and-language-models) 23 | - 🌊 [Kotlin Asynchronous Flow](https://kotlinlang.org/docs/flow.html) support for [StreamingChatLanguageModels](https://docs.langchain4j.dev/tutorials/ai-services#streaming) 24 | - 💄[External Prompt Templates](docs/PromptTemplates.md) support. Basic implementation loads both system and user prompt 25 | templates from the classpath, 26 | but [PromptTemplateSource](langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/PromptTemplateSource.kt) 27 | provides extension mechanism. 28 | - 💾[Async Document Processing Extensions](docs/AsyncIO.md) support parallel document processing with Kotlin coroutines 29 | for efficient I/O operations in LangChain4j 30 | 31 | See [api docs](https://kpavlov.github.io/langchain4j-kotlin/api/) for more details. 32 | 33 | ## Installation 34 | 35 | ### Maven 36 | 37 | Add the following dependencies to your `pom.xml`: 38 | 39 | ```xml 40 | 41 | 42 | 43 | me.kpavlov.langchain4j.kotlin 44 | langchain4j-kotlin 45 | [LATEST_VERSION] 46 | 47 | 48 | 49 | 50 | dev.langchain4j 51 | langchain4j 52 | 1.0.0-beta1 53 | 54 | 55 | dev.langchain4j 56 | langchain4j-open-ai 57 | 1.0.0-beta1 58 | 59 | 60 | ``` 61 | 62 | ### Gradle (Kotlin DSL) 63 | 64 | Add the following to your `build.gradle.kts`: 65 | 66 | ```kotlin 67 | dependencies { 68 | implementation("me.kpavlov.langchain4j.kotlin:langchain4j-kotlin:$LATEST_VERSION") 69 | implementation("dev.langchain4j:langchain4j-open-ai:1.0.0-beta1") 70 | } 71 | ``` 72 | 73 | ## Quick Start 74 | 75 | ### Basic Chat Request 76 | 77 | Extension can convert [`ChatModel`](https://docs.langchain4j.dev/tutorials/chat-and-language-models) response 78 | into [Kotlin Suspending Function](https://kotlinlang.org/docs/coroutines-basics.html): 79 | 80 | ```kotlin 81 | val model: ChatModel = OpenAiChatModel.builder() 82 | .apiKey("your-api-key") 83 | // more configuration parameters here ... 84 | .build() 85 | 86 | // sync call 87 | val response = 88 | model.chat(chatRequest { 89 | messages += systemMessage("You are a helpful assistant") 90 | messages += userMessage("Hello!") 91 | }) 92 | println(response.aiMessage().text()) 93 | 94 | // Using coroutines 95 | CoroutineScope(Dispatchers.IO).launch { 96 | val response = 97 | model.chatAsync { 98 | messages += systemMessage("You are a helpful assistant") 99 | messages += userMessage("Say Hello") 100 | parameters(OpenAiChatRequestParameters.builder()) { 101 | temperature = 0.1 102 | builder.seed(42) // OpenAI specific parameter 103 | } 104 | } 105 | println(response.aiMessage().text()) 106 | } 107 | ``` 108 | 109 | Sample code: 110 | 111 | - [ChatModelExample.kt](samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/ChatModelExample.kt) 112 | - [OpenAiChatModelExample.kt](samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/OpenAiChatModelExample.kt) 113 | 114 | ### Streaming Chat Language Model support 115 | 116 | Extension can convert [StreamingChatModel](https://docs.langchain4j.dev/tutorials/response-streaming) response 117 | into [Kotlin Asynchronous Flow](https://kotlinlang.org/docs/flow.html): 118 | 119 | ```kotlin 120 | val model: StreamingChatModel = OpenAiStreamingChatModel.builder() 121 | .apiKey("your-api-key") 122 | // more configuration parameters here ... 123 | .build() 124 | 125 | model.chatFlow { 126 | messages += systemMessage("You are a helpful assistant") 127 | messages += userMessage("Hello!") 128 | }.collect { reply -> 129 | when (reply) { 130 | is CompleteResponse -> 131 | println( 132 | "Final response: ${reply.response.content().text()}", 133 | ) 134 | 135 | is PartialResponse -> println("Received token: ${reply.token}") 136 | else -> throw IllegalArgumentException("Unsupported event: $reply") 137 | } 138 | } 139 | ``` 140 | 141 | ### Async AI Services 142 | 143 | The library adds support for coroutine-based async AI services through the `AsyncAiServices` class, which leverages 144 | Kotlin's coroutines for efficient asynchronous operations: 145 | 146 | ```kotlin 147 | // Define your service interface with suspending function 148 | interface Assistant { 149 | @UserMessage("Hello, my name is {{name}}. {{question}}") 150 | suspend fun chat(name: String, question: String): String 151 | } 152 | 153 | // Create the service using AsyncAiServicesFactory 154 | val assistant = createAiService( 155 | serviceClass = Assistant::class.java, 156 | factory = AsyncAiServicesFactory(), 157 | ).chatModel(model) 158 | .build() 159 | 160 | // Use with coroutines 161 | runBlocking { 162 | val response = assistant.chat("John", "What is Kotlin?") 163 | println(response) 164 | } 165 | ``` 166 | 167 | #### Advanced Usage Scenarios 168 | 169 | The `AsyncAiServices` implementation uses `HybridVirtualThreadInvocationHandler` under the hood, 170 | which supports multiple invocation patterns: 171 | 172 | 1. **Suspend Functions**: Native Kotlin coroutines support 173 | 2. **CompletionStage/CompletableFuture**: For Java-style async operations 174 | 3. **Blocking Operations**: Automatically run on virtual threads (Java 21+) 175 | 176 | Example with different return types: 177 | 178 | ```kotlin 179 | interface AdvancedAssistant { 180 | // Suspend function 181 | @UserMessage("Summarize: {{text}}") 182 | suspend fun summarize(text: String): String 183 | 184 | // CompletionStage return type for Java interoperability 185 | @UserMessage("Analyze sentiment: {{text}}") 186 | fun analyzeSentiment(text: String): CompletionStage 187 | 188 | // Blocking operation (runs on virtual thread) 189 | @Blocking 190 | @UserMessage("Process document: {{document}}") 191 | fun processDocument(document: String): String 192 | } 193 | ``` 194 | 195 | #### Benefits 196 | 197 | - **Efficient Resource Usage**: Suspending functions don't block threads during I/O or waiting 198 | - **Java Interoperability**: Support for CompletionStage/CompletableFuture return types 199 | - **Virtual Thread Integration**: Automatic handling of blocking operations on virtual threads 200 | - **Simplified Error Handling**: Leverage Kotlin's structured concurrency for error propagation 201 | - **Reduced Boilerplate**: No need for manual callback handling or future chaining 202 | 203 | ### Kotlin Notebook 204 | 205 | The [Kotlin Notebook](https://kotlinlang.org/docs/kotlin-notebook-overview.html) environment allows you to: 206 | 207 | * Experiment with LLM features in real-time 208 | * Test different configurations and scenarios 209 | * Visualize results directly in the notebook 210 | * Share reproducible examples with others 211 | 212 | You can easily get started with LangChain4j-Kotlin notebooks: 213 | 214 | ```kotlin 215 | %useLatestDescriptors 216 | %use coroutines 217 | 218 | @file:DependsOn("dev.langchain4j:langchain4j:0.36.2") 219 | @file:DependsOn("dev.langchain4j:langchain4j-open-ai:0.36.2") 220 | 221 | // add maven dependency 222 | @file:DependsOn("me.kpavlov.langchain4j.kotlin:langchain4j-kotlin:0.1.1") 223 | // ... or add project's target/classes to classpath 224 | //@file:DependsOn("../target/classes") 225 | 226 | import dev.langchain4j.data.message.SystemMessage.systemMessage 227 | import dev.langchain4j.data.message.UserMessage.userMessage 228 | import dev.langchain4j.model.openai.OpenAiChatModel 229 | import kotlinx.coroutines.runBlocking 230 | import kotlinx.coroutines.CoroutineScope 231 | import kotlinx.coroutines.Dispatchers 232 | 233 | import me.kpavlov.langchain4j.kotlin.model.chat.chatAsync 234 | 235 | val model = OpenAiChatModel.builder() 236 | .apiKey("demo") 237 | .modelName("gpt-4o-mini") 238 | .temperature(0.0) 239 | .maxTokens(1024) 240 | .build() 241 | 242 | // Invoke using CoroutineScope 243 | val scope = CoroutineScope(Dispatchers.IO) 244 | 245 | runBlocking { 246 | val result = model.chatAsync { 247 | messages += systemMessage("You are helpful assistant") 248 | messages += userMessage("Make a haiku about Kotlin, Langchain4j and LLM") 249 | } 250 | println(result.content().text()) 251 | } 252 | ``` 253 | 254 | Try [this Kotlin Notebook](langchain4j-kotlin/notebooks/lc4kNotebook.ipynb) yourself: 255 | ![](docs/kotlin-notebook-1.png) 256 | 257 | ## Development Setup 258 | 259 | ### Prerequisites 260 | 261 | 1. Create `.env` file in root directory and add your API keys: 262 | 263 | ```dotenv 264 | OPENAI_API_KEY=sk-xxxxx 265 | ``` 266 | 267 | ### Building the Project 268 | 269 | Using Maven: 270 | 271 | ```shell 272 | mvn clean verify 273 | ``` 274 | 275 | Using Make: 276 | 277 | ```shell 278 | make build 279 | ``` 280 | 281 | ## Contributing 282 | 283 | Contributions are welcome! Please feel free to submit a Pull Request. 284 | 285 | Run before submitting your changes 286 | 287 | ```shell 288 | make lint 289 | ``` 290 | 291 | ## Acknowledgements 292 | 293 | - [LangChain4j](https://github.com/langchain4j/langchain4j) - The core library this project enhances 294 | - Training data from Project Gutenberg: 295 | - [CAPTAIN BLOOD By Rafael Sabatini](https://www.gutenberg.org/cache/epub/1965/pg1965.txt) 296 | 297 | ## License 298 | 299 | [MIT License](LICENSE.txt) 300 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to Release to Maven Central 2 | 3 | 1. Cleanup 4 | 5 | ```shell 6 | mvn release:clean 7 | ``` 8 | 9 | delete git tag, if needed: 10 | 11 | ```shell 12 | git tag -d v0.1.0 13 | ``` 14 | 2. Prepare the release: 15 | 16 | ```shell 17 | mvn release:prepare \ 18 | -Dresume=false \ 19 | -DpushChanges=false 20 | ``` 21 | 3. Perform the release 22 | 23 | ```shell 24 | export GPG_TTY=$(tty) && \ 25 | mvn release:perform -DlocalCheckout=true 26 | ``` 27 | 28 | https://stackoverflow.com/a/57591830/3315474 29 | 30 | In case of GPG error 31 | `gpg: signing failed: Screen or window too small`, [try this](https://stackoverflow.com/a/67498543/3315474): 32 | 33 | ```shell 34 | gpgconf --kill gpg-agent 35 | gpg -K --keyid-format SHORT 36 | echo "test" | gpg --clearsign 37 | ``` 38 | 39 | 4. Push 40 | 41 | ```shell 42 | git push origin 43 | git push origin --tags 44 | ``` 45 | 46 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | style: 2 | active: true 3 | MethodName: 4 | active: true 5 | ignoreOverriddenFunctions: true 6 | excludeAnnotatedMethodsOrClasses: 7 | - "org.junit.jupiter.api.Test" 8 | ignoreTestFiles: true 9 | -------------------------------------------------------------------------------- /docs/AsyncIO.md: -------------------------------------------------------------------------------- 1 | # Parallel Document Processing 2 | 3 | Easy-to-use Kotlin extensions for parallel document processing using coroutines. 4 | 5 | ## Single Document Loading 6 | 7 | ```kotlin 8 | suspend fun loadDocument() { 9 | val source = FileSystemSource(Paths.get("path/to/document.txt")) 10 | val document = loadAsync(source, TextDocumentParser()) 11 | println(document.text()) 12 | } 13 | ``` 14 | 15 | ## Load Multiple Documents in Parallel 16 | 17 | ```kotlin 18 | suspend fun loadDocuments() { 19 | try { 20 | // Get all files from directory 21 | val paths = 22 | Files 23 | .walk(Paths.get("./data")) 24 | .filter(Files::isRegularFile) 25 | .toList() 26 | 27 | // Process each file in parallel 28 | val ioScope = Dispatchers.IO.limitedParallelism(8) 29 | val documentParser = TextDocumentParser() 30 | val documents = 31 | paths 32 | .map { path -> 33 | async { 34 | try { 35 | loadAsync( 36 | source = FileSystemSource(path), 37 | parser = documentParser, 38 | dispatcher = ioScope, 39 | ) 40 | } catch (e: Exception) { 41 | logger.error("Failed to load document: $path", e) 42 | null 43 | } 44 | } 45 | }.awaitAll() 46 | .filterNotNull() 47 | 48 | // Process loaded documents 49 | documents.forEach { doc -> println(doc.text()) } 50 | } catch (e: Exception) { 51 | logger.error("Failed to process documents", e) 52 | throw e 53 | } 54 | } 55 | ``` 56 | 57 | ## Parse from InputStream 58 | 59 | ```kotlin 60 | suspend fun parseInputStream(input: InputStream) { 61 | val parser = TextDocumentParser() 62 | input.use { stream -> // Automatically close stream 63 | val document = parser.parseAsync(stream) 64 | // Process parsed document 65 | println(document.text()) 66 | } 67 | } 68 | ``` 69 | 70 | All operations use `Dispatchers.IO` for optimal I/O performance. 71 | 72 | ## Error Handling Recommendations 73 | 74 | - Each document load operation is isolated - if one fails, others continue 75 | - Use try-catch blocks around individual operations to handle failures gracefully 76 | - Always close resources using [`use`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/use.html) or 77 | try-with-resources 78 | - Log errors for failed operations while allowing successful ones to proceed 79 | 80 | ## Performance Tips 81 | 82 | ### 1.Batch Size 83 | 84 | For large directories, process files in batches to control memory usage 85 | Recommended batch size: 100-1000 documents depending on size 86 | 87 | ### 2. Memory Management: 88 | 89 | Release document references when no longer needed 90 | Consider using sequence for large file sets: Files.walk().asSequence() 91 | 92 | ### 3. Resource Control 93 | 94 | Limit parallel operations based on available CPU cores 95 | Use limitedParallelism for I/O bounds: 96 | 97 | ```kotlin 98 | val ioScope = Dispatchers.IO.limitedParallelism(8) 99 | ``` 100 | 101 | ### 4. Large Files 102 | 103 | Stream large files instead of loading into memory 104 | Consider chunking large documents 105 | 106 | Uses `Dispatchers.IO` for optimal I/O throughput. 107 | -------------------------------------------------------------------------------- /docs/PromptTemplates.md: -------------------------------------------------------------------------------- 1 | # Prompt Templates Guide 2 | 3 | Learn how to use prompt templates with LangChain4J's AiServices. This guide covers setup, configuration, and 4 | customization. 5 | 6 | ## Create Your First Template 7 | 8 | Place your prompt templates in the classpath: 9 | 10 | System prompt template (path: `prompts/default-system-prompt.mustache`): 11 | 12 | ```mustache 13 | You are helpful assistant using chatMemoryID={{chatMemoryID}} 14 | ``` 15 | 16 | User prompt template (path: `prompts/default-user-prompt.mustache`): 17 | 18 | ```mustache 19 | Hello, {{userName}}! {{message}} 20 | ``` 21 | 22 | ## Quick Start 23 | 24 | Here's how to use templates in your code: 25 | 26 | ```kotlin 27 | // Define your assistant 28 | interface Assistant { 29 | @UserMessage("prompts/default-user-prompt.mustache") 30 | fun askQuestion( 31 | @UserName userName: String, // Compile with javac `parameters=true` 32 | @V("message") question: String, 33 | ): String 34 | } 35 | 36 | // Set up the assistant 37 | val assistant: Assistant = 38 | AiServices 39 | .builder(Assistant::class.java) 40 | .systemMessageProvider( 41 | TemplateSystemMessageProvider("prompts/default-system-prompt.mustache") 42 | ).chatModel(model) 43 | .build() 44 | 45 | // Use it 46 | val response = assistant.askQuestion( 47 | userName = "My friend", 48 | question = "How are you?" 49 | ) 50 | ``` 51 | 52 | This creates: 53 | 54 | - System prompt: "You are helpful assistant using chatMemoryID=default" 55 | - User prompt: "Hello, My friend! How are you?" 56 | 57 | ## Under the Hood 58 | 59 | Key components: 60 | 61 | - `PromptTemplateFactory`: Gets templates and handles defaults 62 | - `ClasspathPromptTemplateSource`: Loads templates from your classpath 63 | - `SimpleTemplateRenderer`: Replaces `{{key}}` placeholders with values 64 | - `RenderablePromptTemplate`: Connects everything together 65 | 66 | ## Customize Your Setup 67 | 68 | Configure templates in `langchain4j-kotlin.properties`: 69 | 70 | | Setting | Purpose | Default | 71 | |----------------------------|---------------------------|---------------------------------| 72 | | `prompt.template.source` | Where templates load from | `ClasspathPromptTemplateSource` | 73 | | `prompt.template.renderer` | How templates render | `SimpleTemplateRenderer` | 74 | 75 | ### Add Custom Template Sources 76 | 77 | Create your own source by implementing `PromptTemplateSource`: 78 | 79 | ```kotlin 80 | interface PromptTemplateSource { 81 | fun getTemplate(name: TemplateName): PromptTemplate? 82 | } 83 | ``` 84 | 85 | Example using Redis: 86 | 87 | ```kotlin 88 | class RedisPromptTemplateSource(private val jedis: Jedis) : PromptTemplateSource { 89 | override fun getTemplate(name: TemplateName): PromptTemplate? { 90 | return jedis.get(name)?.let { 91 | SimplePromptTemplate(it) 92 | } 93 | } 94 | } 95 | ``` 96 | 97 | Enable it in your properties: 98 | 99 | ```properties 100 | prompt.template.source=com.example.RedisPromptTemplateSource 101 | ``` 102 | 103 | ### Create Custom Renderers 104 | 105 | Build your own renderer: 106 | 107 | ```kotlin 108 | interface TemplateRenderer { 109 | fun render( 110 | template: TemplateContent, 111 | variables: Map 112 | ): String 113 | } 114 | ``` 115 | 116 | Example: 117 | 118 | ```kotlin 119 | class MyTemplateRenderer : TemplateRenderer { 120 | override fun render(template: TemplateContent, variables: Map): String { 121 | TODO("Add implementation here") 122 | } 123 | } 124 | ``` 125 | 126 | Enable it: 127 | 128 | ```properties 129 | prompt.template.renderer=com.example.MyTemplateRenderer 130 | ``` 131 | 132 | ## Learn More 133 | 134 | Find complete examples: 135 | 136 | - [Unit test example](../langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/ServiceWithPromptTemplatesTest.kt) 137 | - [Using from Java](../samples/src/main/java/me/kpavlov/langchain4j/kotlin/samples/ServiceWithTemplateSourceJavaExample.java) 138 | 139 | -------------------------------------------------------------------------------- /docs/kotlin-notebook-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpavlov/langchain4j-kotlin/006b561d85d75d23e88bae3dc7ff295e1710a916/docs/kotlin-notebook-1.png -------------------------------------------------------------------------------- /langchain4j-kotlin/notebooks/lc4kNotebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "metadata": {}, 5 | "cell_type": "markdown", 6 | "source": "## Welcome to LangChain4j-Kotlin Notebook!" 7 | }, 8 | { 9 | "metadata": { 10 | "ExecuteTime": { 11 | "end_time": "2024-11-22T05:39:35.370383Z", 12 | "start_time": "2024-11-22T05:39:35.118075Z" 13 | } 14 | }, 15 | "cell_type": "code", 16 | "source": [ 17 | "%useLatestDescriptors\n", 18 | "%use coroutines\n", 19 | "\n", 20 | "@file:DependsOn(\"dev.langchain4j:langchain4j:0.36.2\")\n", 21 | "@file:DependsOn(\"dev.langchain4j:langchain4j-open-ai:0.36.2\")\n", 22 | "\n", 23 | "// add maven dependency\n", 24 | "@file:DependsOn(\"me.kpavlov.langchain4j.kotlin:langchain4j-kotlin:0.1.1\")\n", 25 | "// ... or add project's target/classes to classpath\n", 26 | "//@file:DependsOn(\"../target/classes\")" 27 | ], 28 | "outputs": [], 29 | "execution_count": 5 30 | }, 31 | { 32 | "metadata": { 33 | "ExecuteTime": { 34 | "end_time": "2024-11-22T05:39:31.262632Z", 35 | "start_time": "2024-11-22T05:39:29.397246Z" 36 | } 37 | }, 38 | "cell_type": "code", 39 | "source": [ 40 | "import dev.langchain4j.data.message.*\n", 41 | "import dev.langchain4j.model.openai.OpenAiChatModel\n", 42 | "import me.kpavlov.langchain4j.kotlin.model.chat.generateAsync\n", 43 | " \n", 44 | "val model = OpenAiChatModel.builder()\n", 45 | " .apiKey(\"demo\")\n", 46 | " .modelName(\"gpt-4o-mini\")\n", 47 | " .temperature(0.0)\n", 48 | " .maxTokens(1024)\n", 49 | " .build()\n", 50 | "\n", 51 | "// Invoke using CoroutineScope\n", 52 | "val scope = CoroutineScope(Dispatchers.IO)\n", 53 | "\n", 54 | "runBlocking {\n", 55 | " val result = model.generateAsync(\n", 56 | " listOf(\n", 57 | " SystemMessage.from(\"You are helpful assistant\"),\n", 58 | " UserMessage.from(\"Make a haiku about Kotlin, Langchani4j and LLM\"),\n", 59 | " )\n", 60 | " )\n", 61 | " println(result.content().text())\n", 62 | "}" 63 | ], 64 | "outputs": [ 65 | { 66 | "name": "stdout", 67 | "output_type": "stream", 68 | "text": [ 69 | "Kotlin's sleek embrace, \n", 70 | "Langchani4j whispers, \n", 71 | "LLM dreams take flight.\n" 72 | ] 73 | } 74 | ], 75 | "execution_count": 3 76 | }, 77 | { 78 | "metadata": { 79 | "ExecuteTime": { 80 | "end_time": "2024-11-22T05:39:31.268817Z", 81 | "start_time": "2024-11-22T05:39:31.267196Z" 82 | } 83 | }, 84 | "cell_type": "code", 85 | "source": "", 86 | "outputs": [], 87 | "execution_count": null 88 | } 89 | ], 90 | "metadata": { 91 | "kernelspec": { 92 | "display_name": "Kotlin", 93 | "language": "kotlin", 94 | "name": "kotlin" 95 | }, 96 | "language_info": { 97 | "name": "kotlin", 98 | "version": "1.9.23", 99 | "mimetype": "text/x-kotlin", 100 | "file_extension": ".kt", 101 | "pygments_lexer": "kotlin", 102 | "codemirror_mode": "text/x-kotlin", 103 | "nbconvert_exporter": "" 104 | }, 105 | "ktnbPluginMetadata": { 106 | "projectDependencies": [ 107 | "langchain4j-kotlin" 108 | ] 109 | } 110 | }, 111 | "nbformat": 4, 112 | "nbformat_minor": 0 113 | } 114 | -------------------------------------------------------------------------------- /langchain4j-kotlin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | me.kpavlov.langchain4j.kotlin 7 | root 8 | 0.2.1-SNAPSHOT 9 | ../pom.xml 10 | 11 | 12 | langchain4j-kotlin 13 | LangChain4j-Kotlin :: Core 14 | 15 | 16 | 17 | dev.langchain4j 18 | langchain4j-core 19 | compile 20 | 21 | 22 | org.jetbrains.kotlin 23 | kotlin-reflect 24 | 25 | 26 | dev.langchain4j 27 | langchain4j 28 | true 29 | 30 | 31 | org.jetbrains.kotlinx 32 | kotlinx-coroutines-core-jvm 33 | 34 | 35 | org.slf4j 36 | slf4j-api 37 | 38 | 39 | 40 | org.jetbrains.kotlin 41 | kotlin-test-junit5 42 | ${kotlin.version} 43 | test 44 | 45 | 46 | dev.langchain4j 47 | langchain4j-open-ai 48 | test 49 | 50 | 51 | me.kpavlov.finchly 52 | finchly 53 | test 54 | 55 | 56 | org.mockito.kotlin 57 | mockito-kotlin 58 | test 59 | 60 | 61 | org.mockito 62 | mockito-junit-jupiter 63 | test 64 | 65 | 66 | me.kpavlov.aimocks 67 | ai-mocks-openai-jvm 68 | test 69 | 70 | 71 | io.kotest 72 | kotest-assertions-core-jvm 73 | test 74 | 75 | 76 | io.kotest 77 | kotest-assertions-core-jvm 78 | test 79 | 80 | 81 | 82 | 83 | 84 | 85 | org.jetbrains.dokka 86 | dokka-maven-plugin 87 | 88 | 89 | 90 | src/main/kotlin 91 | 92 | https://github.com/kpavlov/langchain4j-kotlin/tree/main/langchain4j-kotlin/src/main/kotlin 93 | 94 | #L 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | linux 105 | 106 | 107 | unix 108 | Linux 109 | 110 | 111 | 112 | 113 | 114 | io.netty 115 | netty-transport-native-epoll 116 | linux-x86_64 117 | runtime 118 | true 119 | 120 | 121 | io.netty 122 | netty-transport-native-epoll 123 | linux-aarch_64 124 | runtime 125 | true 126 | 127 | 128 | 129 | 130 | 131 | mac 132 | 133 | 134 | mac 135 | 136 | 137 | 138 | 139 | 140 | io.netty 141 | netty-transport-native-kqueue 142 | osx-x86_64 143 | runtime 144 | true 145 | 146 | 147 | io.netty 148 | netty-transport-native-kqueue 149 | osx-aarch_64 150 | runtime 151 | true 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/Configuration.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin 2 | 3 | import me.kpavlov.langchain4j.kotlin.prompt.PromptTemplateSource 4 | import me.kpavlov.langchain4j.kotlin.prompt.TemplateRenderer 5 | import java.util.Properties 6 | 7 | /** 8 | * Configuration is an object responsible for loading and providing access to application properties. 9 | * 10 | * This object provides utilities to access various configuration settings and components such as prompt templates and 11 | * their renderers. The configurations are loaded from a properties file, and components are instantiated dynamically 12 | * based on the class names specified in the properties. 13 | */ 14 | public object Configuration { 15 | public val properties: Properties = loadProperties() 16 | 17 | public operator fun get(key: String): String = properties.getProperty(key) 18 | 19 | public val promptTemplateSource: PromptTemplateSource = 20 | createInstanceByName(this["prompt.template.source"]) 21 | public val promptTemplateRenderer: TemplateRenderer = 22 | createInstanceByName(this["prompt.template.renderer"]) 23 | } 24 | 25 | private fun loadProperties(fileName: String = "langchain4j-kotlin.properties"): Properties { 26 | val properties = Properties() 27 | val classLoader = Thread.currentThread().contextClassLoader 28 | classLoader.getResourceAsStream(fileName).use { inputStream -> 29 | require(inputStream != null) { 30 | "Property file '$fileName' not found in the classpath" 31 | } 32 | properties.load(inputStream) 33 | } 34 | return properties 35 | } 36 | 37 | @Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught") 38 | private fun createInstanceByName(className: String): T = 39 | try { 40 | // Get the class object by name 41 | val clazz = Class.forName(className) 42 | // Create an instance of the class 43 | clazz.getDeclaredConstructor().newInstance() as T 44 | } catch (e: Exception) { 45 | throw IllegalArgumentException("Can't create $className", e) 46 | } 47 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/TypeAliases.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin 2 | 3 | /** 4 | * Type alias representing an identifier for a chat memory. 5 | * 6 | * This type alias is used within the `SystemMessageProvider` interface 7 | * and its implementations to specify the input parameter for retrieving 8 | * system messages. 9 | */ 10 | public typealias ChatMemoryId = Any 11 | 12 | /** 13 | * Type alias for the name of a template. 14 | * 15 | * This alias is used to represent template names as strings in various parts 16 | * of the codebase, providing a clearer and more specific meaning compared 17 | * to using `String` directly. 18 | */ 19 | public typealias TemplateName = String 20 | 21 | /** 22 | * Represents the content of a template. 23 | * 24 | * This type alias is used to define a standard type for template content within the system, 25 | * which is expected to be in the form of a string. Various classes and functions that deal 26 | * with templates will utilize this type alias to ensure consistency and clarity. 27 | */ 28 | public typealias TemplateContent = String 29 | 30 | /** 31 | * Type alias for a string representing the content of a prompt. 32 | * 33 | * This alias is used to define the type of content that can be returned 34 | * by various functions and methods within the system that deal with 35 | * generating and handling prompts. 36 | */ 37 | public typealias PromptContent = String 38 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/data/document/DocumentLoaderExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.data.document 2 | 3 | import dev.langchain4j.data.document.Document 4 | import dev.langchain4j.data.document.DocumentLoader 5 | import dev.langchain4j.data.document.DocumentParser 6 | import dev.langchain4j.data.document.DocumentSource 7 | import dev.langchain4j.data.document.source.FileSystemSource 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.async 10 | import kotlinx.coroutines.awaitAll 11 | import kotlinx.coroutines.coroutineScope 12 | import kotlinx.coroutines.withContext 13 | import org.slf4j.LoggerFactory 14 | import java.io.InputStream 15 | import java.nio.file.Files 16 | import java.nio.file.Path 17 | import java.nio.file.PathMatcher 18 | import kotlin.coroutines.CoroutineContext 19 | import kotlin.io.path.exists 20 | import kotlin.io.path.isDirectory 21 | 22 | private val logger = LoggerFactory.getLogger("me.kpavlov.langchain4j.kotlin.data.document") 23 | 24 | /** 25 | * Asynchronously loads a document from the specified source using a given parser. 26 | * 27 | * @param source The [DocumentSource] from which the document will be loaded. 28 | * @param parser The [DocumentParser] to parse the loaded document. 29 | * @param context The [CoroutineContext] to use for asynchronous execution, 30 | * defaults to `Dispatchers.IO`. 31 | * @return The loaded and parsed Document. 32 | */ 33 | public suspend fun loadAsync( 34 | source: DocumentSource, 35 | parser: DocumentParser, 36 | context: CoroutineContext = Dispatchers.IO, 37 | ): Document = 38 | withContext(context) { 39 | DocumentLoader.load(source, parser) 40 | } 41 | 42 | /** 43 | * Asynchronously parses a document from the provided input stream using the specified dispatcher. 44 | * 45 | * @param input The [InputStream] from which the document will be parsed. 46 | * @param context The CoroutineContext to use for asynchronous execution, 47 | * defaults to `[Dispatchers.IO]`. 48 | * @return The parsed Document. 49 | */ 50 | public suspend fun DocumentParser.parseAsync( 51 | input: InputStream, 52 | context: CoroutineContext = Dispatchers.IO, 53 | ): Document = 54 | withContext(context) { 55 | parse(input) 56 | } 57 | 58 | /** 59 | * Asynchronously loads documents from the specified directories. 60 | * 61 | * @param directoryPaths A list of directories from which documents should be loaded. 62 | * @param documentParser The parser to convert files into [Document] objects. 63 | * @param recursive Determines whether subdirectories should also be searched for documents. Defaults to `false`. 64 | * @param pathMatcher An optional filter to match file paths against specific patterns. 65 | * @param context The CoroutineContext to be used for asynchronous operations. Defaults to [Dispatchers.IO]. 66 | * @return A list of Document objects representing the loaded documents. 67 | */ 68 | public suspend fun loadDocumentsAsync( 69 | directoryPaths: List, 70 | documentParser: DocumentParser, 71 | recursive: Boolean = false, 72 | pathMatcher: PathMatcher? = null, 73 | context: CoroutineContext = Dispatchers.IO, 74 | ): List = 75 | coroutineScope { 76 | // Validate all paths before processing 77 | directoryPaths.forEach { path -> 78 | require(path.exists()) { "Path doesn't exist: $path" } 79 | require(path.isDirectory()) { "Path is not a directory: $path" } 80 | } 81 | // Collect all files from the directory paths matching the pathMatcher 82 | val matchedFiles = 83 | directoryPaths.flatMap { path -> 84 | val files = mutableListOf() 85 | // Matches all if no pathMatcher is provided 86 | val matcher: PathMatcher = pathMatcher ?: PathMatcher { true } 87 | 88 | // Traverse directories conditionally based on the recursive flag 89 | val fileStream = if (recursive) Files.walk(path) else Files.walk(path, 1) 90 | 91 | fileStream.use { stream -> 92 | stream 93 | .filter { file -> 94 | Files.isRegularFile(file) && matcher.matches(file) 95 | }.forEach { file -> 96 | files.add(file) 97 | } 98 | } 99 | files 100 | } 101 | 102 | // Process each file in parallel 103 | matchedFiles 104 | .map { file -> 105 | async(context) { 106 | documentParser.parseAsync(FileSystemSource(file), context) 107 | } 108 | }.awaitAll() 109 | .map { document -> 110 | val metadata = document.metadata() 111 | logger.info( 112 | "Loaded document: {}/{}", 113 | metadata.getString(Document.ABSOLUTE_DIRECTORY_PATH), 114 | metadata.getString(Document.FILE_NAME), 115 | ) 116 | document 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/data/document/DocumentParserExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.data.document 2 | 3 | import dev.langchain4j.data.document.Document 4 | import dev.langchain4j.data.document.DocumentParser 5 | import dev.langchain4j.data.document.DocumentSource 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | /** 10 | * Asynchronously parses a document from the specified document source 11 | * using the given coroutine context. 12 | * 13 | * @param source The [DocumentSource] from which the document will be parsed. 14 | * @param context The [CoroutineContext] to use for asynchronous execution, 15 | * defaults to [Dispatchers.IO]. 16 | * @return The parsed [Document], potentially with merged metadata from the document source. 17 | */ 18 | public suspend fun DocumentParser.parseAsync( 19 | source: DocumentSource, 20 | context: CoroutineContext = Dispatchers.IO, 21 | ): Document { 22 | val document = 23 | source.inputStream().use { inputStream -> 24 | return@use parseAsync(inputStream, context) 25 | } 26 | val documentSourceMetadata = source.metadata() 27 | return if (documentSourceMetadata.toMap().isNotEmpty()) { 28 | Document.from(document.text(), documentSourceMetadata.merge(document.metadata())) 29 | } else { 30 | document 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/internal/Logging.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.internal 2 | 3 | import org.slf4j.MarkerFactory 4 | 5 | internal val SENSITIVE = MarkerFactory.getMarker("SENSITIVE") 6 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/adapters/TokenStreamToReplyFlowAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.adapters 2 | 3 | import dev.langchain4j.service.TokenStream 4 | import dev.langchain4j.spi.services.TokenStreamAdapter 5 | import kotlinx.coroutines.flow.Flow 6 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply 7 | import me.kpavlov.langchain4j.kotlin.service.asReplyFlow 8 | import java.lang.reflect.ParameterizedType 9 | import java.lang.reflect.Type 10 | 11 | public class TokenStreamToReplyFlowAdapter : TokenStreamAdapter { 12 | override fun canAdaptTokenStreamTo(type: Type?): Boolean { 13 | if (type is ParameterizedType && type.rawType === Flow::class.java) { 14 | val typeArguments: Array = type.actualTypeArguments 15 | return typeArguments.size == 1 && 16 | typeArguments[0] === StreamingChatModelReply::class.java 17 | } 18 | return false 19 | } 20 | 21 | override fun adapt(tokenStream: TokenStream): Any = tokenStream.asReplyFlow() 22 | } 23 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/adapters/TokenStreamToStringFlowAdapter.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.adapters 2 | 3 | import dev.langchain4j.service.TokenStream 4 | import dev.langchain4j.spi.services.TokenStreamAdapter 5 | import kotlinx.coroutines.flow.Flow 6 | import me.kpavlov.langchain4j.kotlin.service.asFlow 7 | import java.lang.reflect.ParameterizedType 8 | import java.lang.reflect.Type 9 | 10 | public class TokenStreamToStringFlowAdapter : TokenStreamAdapter { 11 | public override fun canAdaptTokenStreamTo(type: Type?): Boolean { 12 | if (type is ParameterizedType) { 13 | if (type.rawType === Flow::class.java) { 14 | val typeArguments: Array = type.actualTypeArguments 15 | return typeArguments.size == 1 && typeArguments[0] === String::class.java 16 | } 17 | } 18 | return false 19 | } 20 | 21 | public override fun adapt(tokenStream: TokenStream): Any = tokenStream.asFlow() 22 | } 23 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatModelExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import dev.langchain4j.model.chat.ChatModel 4 | import dev.langchain4j.model.chat.request.ChatRequest 5 | import dev.langchain4j.model.chat.response.ChatResponse 6 | import kotlinx.coroutines.coroutineScope 7 | import me.kpavlov.langchain4j.kotlin.model.chat.request.ChatRequestBuilder 8 | import me.kpavlov.langchain4j.kotlin.model.chat.request.chatRequest 9 | 10 | /** 11 | * Asynchronously processes a chat request using the language model within 12 | * a coroutine scope. This extension function provides a structured 13 | * concurrency wrapper around the synchronous [ChatModel.chat] method. 14 | * 15 | * Example usage: 16 | * ```kotlin 17 | * val response = model.chatAsync(ChatRequest(messages)) 18 | * ``` 19 | * 20 | * @param request The chat request containing messages and optional parameters 21 | * for the model. 22 | * @return [ChatResponse] containing the model's response and any additional 23 | * metadata. 24 | * @throws Exception if the chat request fails or is interrupted. 25 | * @see ChatModel.chat 26 | * @see ChatRequest 27 | * @see ChatResponse 28 | */ 29 | public suspend fun ChatModel.chatAsync(request: ChatRequest): ChatResponse = 30 | coroutineScope { this@chatAsync.chat(request) } 31 | 32 | /** 33 | * Asynchronously processes a chat request using a [ChatRequest.Builder] for 34 | * convenient request configuration. This extension function combines the 35 | * builder pattern with coroutine-based asynchronous execution. 36 | * 37 | * Example usage: 38 | * ```kotlin 39 | * val response = model.chatAsync(ChatRequest.builder() 40 | * .messages(listOf(UserMessage("Hello"))) 41 | * .temperature(0.7) 42 | * .maxTokens(100)) 43 | * ``` 44 | * 45 | * @param requestBuilder The builder instance configured with desired chat 46 | * request parameters. 47 | * @return [ChatResponse] containing the model's response and any additional 48 | * metadata. 49 | * @throws Exception if the chat request fails, is interrupted, or the builder 50 | * produces an invalid configuration. 51 | * @see ChatRequest 52 | * @see ChatResponse 53 | * @see ChatRequest.Builder 54 | * @see chatAsync 55 | */ 56 | public suspend fun ChatModel.chatAsync(requestBuilder: ChatRequest.Builder): ChatResponse = 57 | chatAsync(requestBuilder.build()) 58 | 59 | /** 60 | * Asynchronously processes a chat request by configuring a `ChatRequest` 61 | * using a provided builder block. This method facilitates the creation 62 | * of well-structured chat requests using a `ChatRequestBuilder` and 63 | * executes the request using the associated `ChatModel`. 64 | * 65 | * Example usage: 66 | * ```kotlin 67 | * model.chatAsync { 68 | * messages += systemMessage("You are a helpful assistant") 69 | * messages += userMessage("Say Hello") 70 | * parameters { 71 | * temperature = 0.1 72 | * } 73 | * } 74 | * ``` 75 | * 76 | * @param block A lambda with receiver on `ChatRequestBuilder` used to 77 | * configure the messages and parameters for the chat request. 78 | * @return A `ChatResponse` containing the response from the model and any 79 | * associated metadata. 80 | * @throws Exception if the chat request fails or encounters an error during execution. 81 | */ 82 | public suspend fun ChatModel.chatAsync(block: ChatRequestBuilder.() -> Unit): ChatResponse = 83 | chatAsync(chatRequest(block)) 84 | 85 | /** 86 | * Processes a chat request using a [ChatRequest.Builder] for convenient request 87 | * configuration. This extension function provides a builder pattern alternative 88 | * to creating [ChatRequest] directly. 89 | * 90 | * Example usage: 91 | * ```kotlin 92 | * val response = model.chat(chatRequest{ 93 | * messages += userMessage("Hello") 94 | * parameters { 95 | * temperature = 0.7 96 | * maxOutputTokens = 100 97 | * } 98 | * }) 99 | * ``` 100 | * 101 | * @param requestBuilder The builder instance configured with desired chat 102 | * request parameters. 103 | * @return [ChatResponse] containing the model's response and any additional 104 | * metadata. 105 | * @throws Exception if the chat request fails or the builder produces an 106 | * invalid configuration. 107 | * @see ChatRequest 108 | * @see ChatResponse 109 | * @see ChatRequest.Builder 110 | * @see ChatRequestBuilder 111 | */ 112 | public fun ChatModel.chat(requestBuilder: ChatRequest.Builder): ChatResponse = 113 | this.chat(requestBuilder.build()) 114 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/StreamingChatModelExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import dev.langchain4j.model.chat.StreamingChatModel 4 | import dev.langchain4j.model.chat.response.ChatResponse 5 | import dev.langchain4j.model.chat.response.StreamingChatResponseHandler 6 | import kotlinx.coroutines.channels.BufferOverflow 7 | import kotlinx.coroutines.channels.Channel 8 | import kotlinx.coroutines.channels.awaitClose 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.buffer 11 | import kotlinx.coroutines.flow.callbackFlow 12 | import me.kpavlov.langchain4j.kotlin.model.chat.request.ChatRequestBuilder 13 | import me.kpavlov.langchain4j.kotlin.model.chat.request.chatRequest 14 | import org.slf4j.LoggerFactory 15 | 16 | private val logger = LoggerFactory.getLogger(StreamingChatModel::class.java) 17 | 18 | /** 19 | * Represents different types of replies that can be received from an AI language model during streaming. 20 | * This sealed interface provides type-safe handling of both intermediate tokens and final completion responses. 21 | */ 22 | public sealed interface StreamingChatModelReply { 23 | /** 24 | * Represents a partial response received from an AI language model during a streaming interaction. 25 | * 26 | * This data class is used to encapsulate an intermediate token that the model generates as part of its 27 | * streaming output. Partial responses are often used in scenarios where the model's output is produced 28 | * incrementally, enabling real-time updates to the user or downstream processes. 29 | * 30 | * @property token The string representation of the token generated as part of the streaming process. 31 | * @see StreamingChatResponseHandler.onPartialResponse 32 | */ 33 | public data class PartialResponse( 34 | val token: String, 35 | ) : StreamingChatModelReply 36 | 37 | /** 38 | * Represents a final completion response received from the AI language model 39 | * during the streaming chat process. 40 | * 41 | * This data class encapsulates the complete response, which typically contains 42 | * the final output of a model's reply in the context of a conversation. 43 | * 44 | * @property response The final chat response generated by the model. 45 | * @see StreamingChatResponseHandler.onCompleteResponse 46 | */ 47 | public data class CompleteResponse( 48 | val response: ChatResponse, 49 | ) : StreamingChatModelReply 50 | 51 | /** 52 | * Represents an error that occurred during the streaming process 53 | * when generating a reply from the AI language model. This type 54 | * of reply is used to indicate a failure in the operation and 55 | * provides details about the cause of the error. 56 | * 57 | * @property cause The underlying exception or error that caused the failure. 58 | * @see StreamingChatResponseHandler.onError 59 | */ 60 | public data class Error( 61 | val cause: Throwable, 62 | ) : StreamingChatModelReply 63 | } 64 | 65 | /** 66 | * Converts a streaming chat language model into a Kotlin [Flow] of [StreamingChatModelReply] 67 | * events. This extension function provides a coroutine-friendly way to consume streaming responses 68 | * from the language model. 69 | * 70 | * The method uses a provided configuration block to build a chat request 71 | * and manages the streaming process by handling partial responses, complete 72 | * responses, and errors through a LC4J's [dev.langchain4j.model.chat.response.StreamingChatResponseHandler]. 73 | * 74 | * @param bufferCapacity The capacity of the buffer used to store incoming tokens. 75 | * Default to [Channel.UNLIMITED] 76 | * @param onBufferOverflow The strategy used to handle buffer overflows when the buffer is full. 77 | * Default to [BufferOverflow.SUSPEND]. The available strategies are: 78 | * - [BufferOverflow.SUSPEND]: Suspends the producer until there is space in the buffer. This is 79 | * suitable for scenarios where maintaining the order of all emitted items is critical. 80 | * - [BufferOverflow.DROP_OLDEST]: Drops the oldest item in the buffer to make space for the new 81 | * item. This is useful when the latest data is more relevant than older data, such as in real-time 82 | * updates or streaming dashboards. 83 | * - [BufferOverflow.DROP_LATEST]: Drops the new item if the buffer is full. This is appropriate 84 | * when older data must be preserved, and losing the latest data is acceptable, such as in logging 85 | * or audit trails. 86 | * @param block A lambda with receiver on [ChatRequestBuilder] used to configure 87 | * the [dev.langchain4j.model.chat.request.ChatRequest] by adding messages and/or setting parameters. 88 | * 89 | * @return A [Flow] of [StreamingChatModelReply], which emits different 90 | * types of replies during the chat interaction, including partial responses, 91 | * final responses, and errors. 92 | */ 93 | @JvmOverloads 94 | public fun StreamingChatModel.chatFlow( 95 | bufferCapacity: Int = Channel.UNLIMITED, 96 | onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, 97 | block: ChatRequestBuilder.() -> Unit, 98 | ): Flow = 99 | callbackFlow { 100 | val model = this@chatFlow 101 | val chatRequest = chatRequest(block) 102 | val handler = 103 | object : StreamingChatResponseHandler { 104 | override fun onPartialResponse(token: String) { 105 | logger.trace( 106 | me.kpavlov.langchain4j.kotlin.internal.SENSITIVE, 107 | "Received partialResponse: {}", 108 | token, 109 | ) 110 | trySend(StreamingChatModelReply.PartialResponse(token)) 111 | } 112 | 113 | override fun onCompleteResponse(completeResponse: ChatResponse) { 114 | logger.trace( 115 | me.kpavlov.langchain4j.kotlin.internal.SENSITIVE, 116 | "Received completeResponse: {}", 117 | completeResponse, 118 | ) 119 | trySend(StreamingChatModelReply.CompleteResponse(completeResponse)) 120 | close() 121 | } 122 | 123 | override fun onError(error: Throwable) { 124 | logger.error( 125 | "Received error: {}", 126 | error.message, 127 | error, 128 | ) 129 | trySend(StreamingChatModelReply.Error(error)) 130 | close(error) 131 | } 132 | } 133 | 134 | logger.info("Starting flow...") 135 | model.chat(chatRequest, handler) 136 | 137 | // This will be called when the flow collection is closed or cancelled. 138 | awaitClose { 139 | // cleanup 140 | logger.info("Flow is canceled") 141 | } 142 | }.buffer(bufferCapacity, onBufferOverflow) 143 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/request/ChatRequestExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat.request 2 | 3 | import dev.langchain4j.agent.tool.ToolSpecification 4 | import dev.langchain4j.data.message.ChatMessage 5 | import dev.langchain4j.model.chat.request.ChatRequest 6 | import dev.langchain4j.model.chat.request.ChatRequestParameters 7 | import dev.langchain4j.model.chat.request.DefaultChatRequestParameters 8 | import dev.langchain4j.model.chat.request.ResponseFormat 9 | import dev.langchain4j.model.chat.request.ToolChoice 10 | 11 | /** 12 | * Builds and returns a `ChatRequest` using the provided configuration block. 13 | * The configuration is applied on a `ChatRequestBuilder` instance to customize 14 | * messages and parameters that will be part of the resulting `ChatRequest`. 15 | * 16 | * @param block A lambda with receiver on `ChatRequestBuilder` to configure messages 17 | * and/or parameters for the `ChatRequest`. 18 | * @return A fully constructed `ChatRequest` instance based on the applied configurations. 19 | */ 20 | public fun chatRequest(block: ChatRequestBuilder.() -> Unit): ChatRequest { 21 | val builder = ChatRequestBuilder() 22 | builder.apply { block() } 23 | return builder.build() 24 | } 25 | 26 | /** 27 | * A utility class for building and configuring chat request parameters. This builder allows fine-grained 28 | * control over various fields such as model configuration, response shaping, and tool integration. 29 | * 30 | * @param B The type of the builder for the default chat request parameters. 31 | * @property builder The builder used to configure the chat request parameters. 32 | * @property modelName Specifies the name of the model to be used for the chat request. 33 | * @property temperature Controls the randomness in the response generation. Higher values produce more random outputs. 34 | * @property topP Configures nucleus sampling, limiting the selection to a subset of tokens 35 | * with a cumulative probability of `topP`. 36 | * @property topK Limits the selection to the top `K` tokens during response generation. 37 | * @property frequencyPenalty Applies a penalty to discourage repetition of tokens based on frequency. 38 | * @property presencePenalty Applies a penalty to encourage diversity by penalizing token presence 39 | * in the conversation context. 40 | * @property maxOutputTokens Specifies the maximum number of tokens for the generated response. 41 | * @property stopSequences A list of sequences that will terminate the response generation if encountered. 42 | * @property toolSpecifications A list of tool specifications for integrating external tools into the chat request. 43 | * @property toolChoice Defines the specific tool to be used if multiple tools are available in the request. 44 | * @property responseFormat Specifies the format of the response, such as plain text or structured data. 45 | */ 46 | @Suppress("LongParameterList") 47 | public open class ChatRequestParametersBuilder>( 48 | public val builder: B, 49 | public var modelName: String? = null, 50 | public var temperature: Double? = null, 51 | public var topP: Double? = null, 52 | public var topK: Int? = null, 53 | public var frequencyPenalty: Double? = null, 54 | public var presencePenalty: Double? = null, 55 | public var maxOutputTokens: Int? = null, 56 | public var stopSequences: List? = null, 57 | public var toolSpecifications: List? = null, 58 | public var toolChoice: ToolChoice? = null, 59 | public var responseFormat: ResponseFormat? = null, 60 | ) 61 | 62 | /** 63 | * Builder class for constructing a `ChatRequest` instance. Allows configuring 64 | * messages and request parameters to customize the resulting request. 65 | * 66 | * This builder provides methods to add individual or multiple chat messages, 67 | * as well as set request parameters for the generated `ChatRequest`. 68 | */ 69 | public open class ChatRequestBuilder( 70 | public var messages: MutableList = mutableListOf(), 71 | public var parameters: ChatRequestParameters? = null, 72 | ) { 73 | /** 74 | * Adds a list of `ChatMessage` objects to the builder's messages collection. 75 | * 76 | * @param value The list of `ChatMessage` objects to be added to the builder. 77 | * @return This builder instance for chaining other method calls. 78 | */ 79 | public fun messages(value: List): ChatRequestBuilder = 80 | apply { this.messages.addAll(value) } 81 | 82 | /** 83 | * Adds a chat message to the messages list. 84 | * 85 | * @param value The chat message to be added. 86 | * @return The current instance for method chaining. 87 | */ 88 | public fun message(value: ChatMessage): ChatRequestBuilder = apply { this.messages.add(value) } 89 | 90 | /** 91 | * Builds and returns a ChatRequest instance using the current state of messages and parameters. 92 | * 93 | * @return A new instance of ChatRequest configured with the provided messages and parameters. 94 | */ 95 | internal fun build(): ChatRequest = 96 | ChatRequest 97 | .Builder() 98 | .messages(this.messages) 99 | .parameters(this.parameters) 100 | .build() 101 | 102 | /** 103 | * Configures and sets the parameters for the chat request. 104 | * 105 | * @param builder The builder instance used to create the chat request parameters. 106 | * Defaults to an instance of [DefaultChatRequestParameters.Builder]. 107 | * @param configurer A lambda with the builder as receiver to configure the chat request parameters. 108 | */ 109 | @JvmOverloads 110 | public fun > parameters( 111 | @Suppress("UNCHECKED_CAST") 112 | builder: B = DefaultChatRequestParameters.builder() as B, 113 | configurer: ChatRequestParametersBuilder.() -> Unit, 114 | ) { 115 | val b = ChatRequestParametersBuilder(builder = builder).also(configurer) 116 | parameters = 117 | builder 118 | .apply { 119 | b.modelName?.let { modelName(it) } 120 | b.temperature?.let { temperature(it) } 121 | b.topP?.let { topP(it) } 122 | b.topK?.let { topK(it) } 123 | b.frequencyPenalty?.let { frequencyPenalty(it) } 124 | b.presencePenalty?.let { presencePenalty(it) } 125 | b.maxOutputTokens?.let { maxOutputTokens(it) } 126 | b.stopSequences?.let { stopSequences(it) } 127 | b.toolSpecifications?.let { toolSpecifications(it) } 128 | b.toolChoice?.let { toolChoice(it) } 129 | b.responseFormat?.let { responseFormat(it) } 130 | }.build() 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/ClasspathPromptTemplateSource.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import me.kpavlov.langchain4j.kotlin.TemplateName 4 | 5 | /** 6 | * Classpath-based implementation of [PromptTemplateSource]. 7 | * 8 | * This class provides a mechanism to load prompt templates from the classpath 9 | * using the template name as the resource identifier. It attempts to locate the 10 | * template file in the classpath and reads its contents as the template data. 11 | */ 12 | public open class ClasspathPromptTemplateSource : PromptTemplateSource { 13 | /** 14 | * Retrieves a prompt template based on the provided template name. 15 | * 16 | * This method attempts to locate the template file in the classpath using the given 17 | * template name as the resource identifier. If found, it reads the contents of the 18 | * file and returns a [SimplePromptTemplate] containing the template data. 19 | * 20 | * @param name The name of the template to retrieve. 21 | * @return The prompt template associated with the specified name, or null if no such template exists. 22 | */ 23 | override fun getTemplate(name: TemplateName): PromptTemplate? { 24 | val resourceStream = this::class.java.classLoader.getResourceAsStream(name) 25 | return resourceStream?.bufferedReader()?.use { reader -> 26 | val content = reader.readText() 27 | return SimplePromptTemplate(content) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/PromptTemplate.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import me.kpavlov.langchain4j.kotlin.TemplateContent 4 | 5 | /** 6 | * Interface representing a template for prompts. 7 | * 8 | * Implementations of this interface provide the template content that can be used 9 | * to generate prompts. The content is expected to be a string format that can 10 | * incorporate variables or placeholders. 11 | */ 12 | public interface PromptTemplate { 13 | public fun content(): TemplateContent 14 | } 15 | 16 | /** 17 | * Data class representing a simple implementation of the [PromptTemplate] interface. 18 | * 19 | * This class provides a concrete implementation of the [PromptTemplate] interface by 20 | * storing the template content and returning it via the `content` method. It is 21 | * designed to work with prompt templates loaded from various sources. 22 | * 23 | * @param content The content of the template, represented as [TemplateContent]. 24 | */ 25 | public data class SimplePromptTemplate( 26 | private val content: TemplateContent, 27 | ) : PromptTemplate { 28 | public override fun content(): TemplateContent = content 29 | } 30 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/PromptTemplateFactory.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import dev.langchain4j.spi.prompt.PromptTemplateFactory 4 | import me.kpavlov.langchain4j.kotlin.Configuration 5 | import me.kpavlov.langchain4j.kotlin.internal.SENSITIVE 6 | import org.slf4j.Logger 7 | 8 | /** 9 | * Factory class for creating instances of [RenderablePromptTemplate]. 10 | * 11 | * This class is responsible for obtaining prompt templates from a [PromptTemplateSource] 12 | * and rendering them using a [TemplateRenderer]. If the specified template cannot be found, 13 | * it will fallback to using the input template content. 14 | * 15 | * @constructor Creates an instance of [PromptTemplateFactory] using the provided source and renderer. 16 | * @property logger Logger instance for logging information and debugging messages. 17 | * @property source Source to obtain prompt templates by their name. 18 | * @property renderer Renderer used to render the templates with provided variables. 19 | */ 20 | public open class PromptTemplateFactory : PromptTemplateFactory { 21 | protected val logger: Logger = org.slf4j.LoggerFactory.getLogger(javaClass) 22 | 23 | private val source: PromptTemplateSource = Configuration.promptTemplateSource 24 | private val renderer: TemplateRenderer = Configuration.promptTemplateRenderer 25 | 26 | override fun create(input: PromptTemplateFactory.Input): PromptTemplateFactory.Template { 27 | logger.info( 28 | "Create PromptTemplate input.template = ${input.template}, input.name = ${input.name}", 29 | ) 30 | val template = source.getTemplate(input.template) 31 | return if (template == null) { 32 | if (logger.isTraceEnabled) { 33 | logger.trace( 34 | SENSITIVE, 35 | "Prompt template not found, failing back to input.template=\"{}\"", 36 | input.template, 37 | ) 38 | } else { 39 | logger.debug( 40 | "Prompt template not found, failing back to input.template", 41 | ) 42 | } 43 | RenderablePromptTemplate( 44 | name = input.name, 45 | content = input.template, 46 | templateRenderer = renderer, 47 | ) 48 | } else { 49 | if (logger.isTraceEnabled) { 50 | logger.trace( 51 | "Found Prompt template by name=\"{}\": \"{}\"", 52 | input.template, 53 | template, 54 | ) 55 | } else { 56 | logger.debug( 57 | "Found Prompt template by name=\"{}\"", 58 | input.template, 59 | ) 60 | } 61 | RenderablePromptTemplate( 62 | name = input.template, 63 | content = template.content(), 64 | templateRenderer = renderer, 65 | ) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/PromptTemplateSource.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import me.kpavlov.langchain4j.kotlin.TemplateName 4 | 5 | /** 6 | * Interface for obtaining prompt templates by their name. 7 | * 8 | * This interface defines a method for retrieving a prompt template using 9 | * a template name. The implementation of this interface will determine 10 | * how and from where the templates are sourced. 11 | */ 12 | public interface PromptTemplateSource { 13 | /** 14 | * Retrieves a prompt template based on the provided template name. 15 | * 16 | * @param name The name of the template to retrieve. 17 | * @return The prompt template associated with the specified name, or null if no such template exists. 18 | */ 19 | public fun getTemplate(name: TemplateName): PromptTemplate? 20 | } 21 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/RenderablePromptTemplate.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import me.kpavlov.langchain4j.kotlin.TemplateContent 4 | import me.kpavlov.langchain4j.kotlin.TemplateName 5 | import org.slf4j.LoggerFactory 6 | 7 | private val logger = LoggerFactory.getLogger(RenderablePromptTemplate::class.java) 8 | 9 | /** 10 | * Represents a renderable template for prompts. 11 | * 12 | * This class implements both [PromptTemplate] and LC4J's `PromptTemplateFactory.Template` interfaces. 13 | * It uses a [TemplateRenderer] to render the template content using provided variables. 14 | * 15 | * @property name The name of the template. 16 | * @property content The content of the template. 17 | * @property templateRenderer The renderer used for generating the final template string from the content and variables. 18 | */ 19 | public class RenderablePromptTemplate( 20 | public val name: TemplateName, 21 | private val content: TemplateContent, 22 | private val templateRenderer: TemplateRenderer, 23 | ) : PromptTemplate, 24 | dev.langchain4j.spi.prompt.PromptTemplateFactory.Template { 25 | override fun content(): TemplateContent = content 26 | 27 | override fun render(variables: Map): String { 28 | logger.info("Rendering template: {}", name) 29 | return templateRenderer.render(template = content, variables = variables) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/SimpleTemplateRenderer.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import me.kpavlov.langchain4j.kotlin.TemplateContent 4 | 5 | /** 6 | * A simple implementation of the TemplateRenderer interface that replaces placeholders 7 | * in the provided template with corresponding variable values. 8 | * 9 | * The placeholders in the template should be in the format `{{key}}`, where `key` corresponds 10 | * to an entry in the variables map. 11 | * 12 | * If any placeholders in the template are not defined in the variables map, an IllegalArgumentException 13 | * will be thrown. 14 | */ 15 | public class SimpleTemplateRenderer : TemplateRenderer { 16 | public override fun render( 17 | template: TemplateContent, 18 | variables: Map, 19 | ): String { 20 | val undefinedKeys = 21 | "\\{\\{(\\w+)\\}\\}" 22 | .toRegex() 23 | .findAll(template) 24 | .map { it.groupValues[1] } 25 | .filterNot { variables.containsKey(it) } 26 | .toList() 27 | 28 | require(undefinedKeys.isEmpty()) { 29 | "Undefined keys in template: ${ 30 | undefinedKeys.joinToString( 31 | ", ", 32 | ) 33 | }" 34 | } 35 | 36 | return variables.entries.fold(template) { acc, entry -> 37 | acc.replace("{{${entry.key}}}", entry.value?.toString() ?: "") 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/TemplateRenderer.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import me.kpavlov.langchain4j.kotlin.TemplateContent 4 | 5 | /** 6 | * Interface for rendering a text template with provided variables. 7 | * 8 | * Implementers of this interface will typically replace placeholders in the template 9 | * with corresponding values from the variables map. 10 | */ 11 | public interface TemplateRenderer { 12 | public fun render( 13 | template: TemplateContent, 14 | variables: Map, 15 | ): String 16 | } 17 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/rag/RetrievalAugmentorExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.rag 2 | 3 | import dev.langchain4j.rag.AugmentationRequest 4 | import dev.langchain4j.rag.AugmentationResult 5 | import dev.langchain4j.rag.RetrievalAugmentor 6 | import kotlinx.coroutines.coroutineScope 7 | 8 | public suspend fun RetrievalAugmentor.augmentAsync( 9 | request: AugmentationRequest, 10 | ): AugmentationResult = coroutineScope { this@augmentAsync.augment(request) } 11 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/AiServiceOrchestrator.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.data.message.ChatMessage 4 | import dev.langchain4j.data.message.SystemMessage 5 | import dev.langchain4j.data.message.UserMessage 6 | import dev.langchain4j.internal.Utils 7 | import dev.langchain4j.memory.ChatMemory 8 | import dev.langchain4j.model.chat.Capability 9 | import dev.langchain4j.model.chat.request.ChatRequest 10 | import dev.langchain4j.model.chat.request.ChatRequestParameters 11 | import dev.langchain4j.model.chat.request.ResponseFormat 12 | import dev.langchain4j.model.chat.request.ResponseFormatType 13 | import dev.langchain4j.model.chat.request.json.JsonSchema 14 | import dev.langchain4j.model.input.PromptTemplate 15 | import dev.langchain4j.model.moderation.Moderation 16 | import dev.langchain4j.rag.AugmentationRequest 17 | import dev.langchain4j.rag.AugmentationResult 18 | import dev.langchain4j.rag.query.Metadata 19 | import dev.langchain4j.service.AiServiceContext 20 | import dev.langchain4j.service.AiServiceTokenStream 21 | import dev.langchain4j.service.AiServiceTokenStreamParameters 22 | import dev.langchain4j.service.AiServices 23 | import dev.langchain4j.service.IllegalConfigurationException 24 | import dev.langchain4j.service.Moderate 25 | import dev.langchain4j.service.Result 26 | import dev.langchain4j.service.TokenStream 27 | import dev.langchain4j.service.TypeUtils 28 | import dev.langchain4j.service.memory.ChatMemoryAccess 29 | import dev.langchain4j.service.memory.ChatMemoryService 30 | import dev.langchain4j.service.output.ServiceOutputParser 31 | import dev.langchain4j.service.tool.ToolServiceContext 32 | import dev.langchain4j.spi.services.TokenStreamAdapter 33 | import me.kpavlov.langchain4j.kotlin.ChatMemoryId 34 | import me.kpavlov.langchain4j.kotlin.model.chat.chatAsync 35 | import me.kpavlov.langchain4j.kotlin.service.ReflectionHelper.validateParameters 36 | import me.kpavlov.langchain4j.kotlin.service.ReflectionVariableResolver.asString 37 | import me.kpavlov.langchain4j.kotlin.service.ReflectionVariableResolver.findMemoryId 38 | import me.kpavlov.langchain4j.kotlin.service.ReflectionVariableResolver.findTemplateVariables 39 | import me.kpavlov.langchain4j.kotlin.service.ReflectionVariableResolver.findUserMessageTemplateFromTheOnlyArgument 40 | import me.kpavlov.langchain4j.kotlin.service.ReflectionVariableResolver.findUserName 41 | import me.kpavlov.langchain4j.kotlin.service.memory.evictChatMemoryAsync 42 | import me.kpavlov.langchain4j.kotlin.service.memory.getChatMemoryAsync 43 | import me.kpavlov.langchain4j.kotlin.service.memory.getOrCreateChatMemoryAsync 44 | import org.jetbrains.annotations.ApiStatus 45 | import java.io.InputStream 46 | import java.lang.reflect.Method 47 | import java.lang.reflect.Parameter 48 | import java.lang.reflect.Type 49 | import java.util.Optional 50 | import java.util.Scanner 51 | import java.util.concurrent.Callable 52 | import java.util.concurrent.ExecutorService 53 | import java.util.concurrent.Executors 54 | import java.util.concurrent.Future 55 | import java.util.function.Supplier 56 | 57 | @ApiStatus.Internal 58 | @Suppress("TooManyFunctions", "detekt:all") 59 | internal class AiServiceOrchestrator( 60 | private val context: AiServiceContext, 61 | private val serviceOutputParser: ServiceOutputParser, 62 | private val tokenStreamAdapters: Collection, 63 | ) { 64 | private val executor: ExecutorService = Executors.newCachedThreadPool() 65 | 66 | @Throws(Exception::class) 67 | @Suppress( 68 | "LongMethod", 69 | "CyclomaticComplexMethod", 70 | "ReturnCount", 71 | "UnusedParameter", 72 | "UseCheckOrError", 73 | "ComplexCondition", 74 | ) 75 | suspend fun execute( 76 | method: Method, 77 | args: Array, 78 | ): Any? { 79 | if (method.declaringClass == Any::class.java) { 80 | // methods like equals(), hashCode() and toString() should not be handled by this proxy 81 | return method.invoke(this, *args) 82 | } 83 | 84 | val chatMemoryService = context.chatMemoryService 85 | if (method.declaringClass == ChatMemoryAccess::class.java && args.size >= 1) { 86 | return when (method.name) { 87 | "getChatMemory" -> chatMemoryService.getChatMemoryAsync(args[0]!!) 88 | "evictChatMemory" -> { 89 | chatMemoryService.evictChatMemoryAsync(args[0]!!) != null 90 | } 91 | 92 | else -> throw UnsupportedOperationException( 93 | "Unknown method on ChatMemoryAccess class: ${method.name}", 94 | ) 95 | } 96 | } 97 | 98 | validateParameters(method) 99 | 100 | val memoryId = 101 | findMemoryId(method, args) 102 | .orElse(ChatMemoryService.DEFAULT) 103 | val chatMemory = 104 | if (context.hasChatMemory()) { 105 | chatMemoryService.getOrCreateChatMemoryAsync(memoryId) 106 | } else { 107 | null 108 | } 109 | 110 | val systemMessage = prepareSystemMessage(memoryId, method, args) 111 | var userMessage = prepareUserMessage(method, args) 112 | var augmentationResult: AugmentationResult? = null 113 | 114 | context.retrievalAugmentor?.let { 115 | val chatMemoryMessages = chatMemory?.messages() 116 | val metadata = Metadata.from(userMessage, memoryId, chatMemoryMessages) 117 | val augmentationRequest = AugmentationRequest(userMessage, metadata) 118 | augmentationResult = it.augment(augmentationRequest) 119 | userMessage = augmentationResult?.chatMessage() as UserMessage 120 | } 121 | 122 | val returnType = ReflectionHelper.getSuspendReturnType(method) 123 | val streaming = returnType == TokenStream::class.java || canAdaptTokenStreamTo(returnType) 124 | val supportsJsonSchema = supportsJsonSchema() 125 | var jsonSchema: JsonSchema? = null 126 | 127 | if (supportsJsonSchema && !streaming) { 128 | jsonSchema = serviceOutputParser.jsonSchema(returnType).orElse(null) 129 | } 130 | 131 | if ((jsonSchema == null) && !streaming) { 132 | // TODO append after storing in the memory? 133 | userMessage = appendOutputFormatInstructions(returnType, userMessage) 134 | } 135 | 136 | val messages = 137 | if (chatMemory != null) { 138 | systemMessage?.let { chatMemory::add } 139 | chatMemory.add(userMessage) 140 | chatMemory.messages() 141 | } else { 142 | mutableListOf().apply { 143 | systemMessage.let(this::add) 144 | add(userMessage) 145 | } 146 | } 147 | 148 | val toolServiceContext = 149 | context.toolService.createContext(memoryId, userMessage) 150 | 151 | return if (streaming) { 152 | handleStreamingCall( 153 | returnType, 154 | messages, 155 | toolServiceContext, 156 | augmentationResult, 157 | memoryId, 158 | ) 159 | } else { 160 | val moderationFuture = triggerModerationIfNeeded(method, messages) 161 | 162 | handleNonStreamingCall( 163 | returnType, 164 | messages, 165 | toolServiceContext, 166 | augmentationResult, 167 | moderationFuture, 168 | chatMemory, 169 | memoryId, 170 | supportsJsonSchema, 171 | jsonSchema, 172 | ) 173 | } 174 | } 175 | 176 | private fun handleStreamingCall( 177 | returnType: Type, 178 | messages: MutableList, 179 | toolServiceContext: ToolServiceContext, 180 | augmentationResult: AugmentationResult?, 181 | memoryId: Any, 182 | ): Any? { 183 | val tokenStream = 184 | AiServiceTokenStream( 185 | AiServiceTokenStreamParameters 186 | .builder() 187 | .messages(messages) 188 | .toolSpecifications(toolServiceContext.toolSpecifications()) 189 | .toolExecutors(toolServiceContext.toolExecutors()) 190 | .retrievedContents(augmentationResult?.contents()) 191 | .context(context) 192 | .memoryId(memoryId) 193 | .build(), 194 | ) 195 | // TODO moderation 196 | return when { 197 | returnType == TokenStream::class.java -> tokenStream 198 | else -> adapt(tokenStream, returnType) 199 | } 200 | } 201 | 202 | @Suppress("LongParameterList") 203 | private suspend fun handleNonStreamingCall( 204 | returnType: Type, 205 | messages: MutableList, 206 | toolServiceContext: ToolServiceContext, 207 | augmentationResult: AugmentationResult?, 208 | moderationFuture: Future?, 209 | chatMemory: ChatMemory?, 210 | memoryId: ChatMemoryId, 211 | supportsJsonSchema: Boolean, 212 | jsonSchema: JsonSchema?, 213 | ): Any? { 214 | val responseFormat = 215 | if (supportsJsonSchema && jsonSchema != null) { 216 | ResponseFormat 217 | .builder() 218 | .type(ResponseFormatType.JSON) 219 | .jsonSchema(jsonSchema) 220 | .build() 221 | } else { 222 | null 223 | } 224 | 225 | val parameters = 226 | ChatRequestParameters 227 | .builder() 228 | .toolSpecifications(toolServiceContext.toolSpecifications()) 229 | .responseFormat(responseFormat) 230 | .build() 231 | 232 | val chatRequest = 233 | ChatRequest 234 | .builder() 235 | .messages(messages) 236 | .parameters(parameters) 237 | .build() 238 | 239 | var chatResponse = context.chatModel.chatAsync(chatRequest) 240 | 241 | AiServices.verifyModerationIfNeeded(moderationFuture) 242 | 243 | val toolExecutionResult = 244 | context.toolService.executeInferenceAndToolsLoop( 245 | chatResponse, 246 | parameters, 247 | messages, 248 | context.chatModel, 249 | chatMemory, 250 | memoryId, 251 | toolServiceContext.toolExecutors(), 252 | ) 253 | 254 | chatResponse = toolExecutionResult.chatResponse() 255 | val finishReason = chatResponse.metadata().finishReason() 256 | 257 | val parsedResponse = serviceOutputParser.parse(chatResponse, returnType) 258 | return if (TypeUtils.typeHasRawClass(returnType, Result::class.java)) { 259 | Result 260 | .builder() 261 | .content(parsedResponse) 262 | .tokenUsage(chatResponse.tokenUsage()) 263 | .sources(augmentationResult?.contents()) 264 | .finishReason(finishReason) 265 | .toolExecutions(toolExecutionResult.toolExecutions()) 266 | .build() 267 | } else { 268 | parsedResponse 269 | } 270 | } 271 | 272 | private fun canAdaptTokenStreamTo(returnType: Type): Boolean = 273 | tokenStreamAdapters.any { it.canAdaptTokenStreamTo(returnType) } 274 | 275 | private fun adapt( 276 | tokenStream: TokenStream, 277 | returnType: Type, 278 | ): Any? = 279 | tokenStreamAdapters 280 | .firstOrNull { it.canAdaptTokenStreamTo(returnType) } 281 | ?.adapt(tokenStream) 282 | ?: throw IllegalStateException("Can't find suitable TokenStreamAdapter") 283 | 284 | private fun supportsJsonSchema(): Boolean = 285 | context.chatModel 286 | ?.supportedCapabilities() 287 | ?.contains(Capability.RESPONSE_FORMAT_JSON_SCHEMA) ?: false 288 | 289 | private fun appendOutputFormatInstructions( 290 | returnType: Type, 291 | userMessage: UserMessage, 292 | ): UserMessage { 293 | val outputFormatInstructions = serviceOutputParser.outputFormatInstructions(returnType) 294 | val text = userMessage.singleText() + outputFormatInstructions 295 | return if (Utils.isNotNullOrBlank(userMessage.name())) { 296 | UserMessage.from(userMessage.name(), text) 297 | } else { 298 | UserMessage.from(text) 299 | } 300 | } 301 | 302 | private fun triggerModerationIfNeeded( 303 | method: Method, 304 | messages: MutableList, 305 | ): Future? = 306 | if (method.isAnnotationPresent(Moderate::class.java)) { 307 | executor.submit( 308 | Callable { 309 | val messagesToModerate = AiServices.removeToolMessages(messages) 310 | context.moderationModel 311 | .moderate(messagesToModerate) 312 | .content() 313 | }, 314 | ) 315 | } else { 316 | null 317 | } 318 | 319 | private fun prepareSystemMessage( 320 | memoryId: Any?, 321 | method: Method, 322 | args: Array, 323 | ): SystemMessage? = 324 | findSystemMessageTemplate(memoryId, method) 325 | .map { systemMessageTemplate: String -> 326 | PromptTemplate 327 | .from(systemMessageTemplate) 328 | .apply( 329 | findTemplateVariables( 330 | systemMessageTemplate, 331 | method, 332 | args, 333 | ), 334 | ).toSystemMessage() 335 | }.orElse(null) 336 | 337 | private fun findSystemMessageTemplate( 338 | memoryId: ChatMemoryId?, 339 | method: Method, 340 | ): Optional { 341 | val annotation = 342 | method.getAnnotation( 343 | dev.langchain4j.service.SystemMessage::class.java, 344 | ) 345 | if (annotation != null) { 346 | return Optional.of( 347 | getTemplate( 348 | method, 349 | "System", 350 | annotation.fromResource, 351 | annotation.value, 352 | annotation.delimiter, 353 | ), 354 | ) 355 | } 356 | 357 | return context.systemMessageProvider.apply(memoryId) 358 | } 359 | 360 | private fun getTemplate( 361 | method: Method, 362 | type: String?, 363 | resource: String, 364 | value: Array, 365 | delimiter: String, 366 | ): String { 367 | val messageTemplate: String = 368 | if (!resource.trim { it <= ' ' }.isEmpty()) { 369 | val resourceText = getResourceText(method.declaringClass, resource) 370 | if (resourceText == null) { 371 | throw IllegalConfigurationException.illegalConfiguration( 372 | "@%sMessage's resource '%s' not found", 373 | type, 374 | resource, 375 | ) 376 | } 377 | resourceText 378 | } else { 379 | java.lang.String.join(delimiter, *value) 380 | } 381 | if (messageTemplate.trim { it <= ' ' }.isEmpty()) { 382 | throw IllegalConfigurationException.illegalConfiguration( 383 | "@%sMessage's template cannot be empty", 384 | type, 385 | ) 386 | } 387 | return messageTemplate 388 | } 389 | 390 | private fun getResourceText( 391 | clazz: Class<*>, 392 | resource: String, 393 | ): String? { 394 | var inputStream = clazz.getResourceAsStream(resource) 395 | if (inputStream == null) { 396 | inputStream = clazz.getResourceAsStream("/$resource") 397 | } 398 | return getText(inputStream) 399 | } 400 | 401 | private fun getText(inputStream: InputStream?): String? { 402 | if (inputStream == null) { 403 | return null 404 | } 405 | Scanner(inputStream).use { scanner -> 406 | scanner.useDelimiter("\\A").use { s -> 407 | return if (s.hasNext()) s.next() else "" 408 | } 409 | } 410 | } 411 | 412 | private fun prepareUserMessage( 413 | method: Method, 414 | args: Array, 415 | ): UserMessage { 416 | val template = getUserMessageTemplate(method, args) 417 | val variables = findTemplateVariables(template, method, args) 418 | 419 | val prompt = PromptTemplate.from(template).apply(variables) 420 | 421 | val maybeUserName = findUserName(method.parameters, args) 422 | return maybeUserName 423 | .map { userName: String? -> 424 | UserMessage.from( 425 | userName, 426 | prompt.text(), 427 | ) 428 | }.orElseGet(Supplier { prompt.toUserMessage() }) 429 | } 430 | 431 | private fun getUserMessageTemplate( 432 | method: Method, 433 | args: Array, 434 | ): String { 435 | val templateFromMethodAnnotation = 436 | findUserMessageTemplateFromMethodAnnotation(method) 437 | val templateFromParameterAnnotation = 438 | findUserMessageTemplateFromAnnotatedParameter( 439 | method.parameters, 440 | args, 441 | ) 442 | 443 | if (templateFromMethodAnnotation.isPresent && 444 | templateFromParameterAnnotation.isPresent 445 | ) { 446 | throw IllegalConfigurationException.illegalConfiguration( 447 | "Error: The method '%s' has multiple @UserMessage annotations. Please use only one.", 448 | method.name, 449 | ) 450 | } 451 | 452 | if (templateFromMethodAnnotation.isPresent) { 453 | return templateFromMethodAnnotation.get() 454 | } 455 | if (templateFromParameterAnnotation.isPresent) { 456 | return templateFromParameterAnnotation.get() 457 | } 458 | 459 | val templateFromTheOnlyArgument = 460 | findUserMessageTemplateFromTheOnlyArgument( 461 | method.parameters, 462 | args, 463 | ) 464 | if (templateFromTheOnlyArgument.isPresent) { 465 | return templateFromTheOnlyArgument.get() 466 | } 467 | 468 | throw IllegalConfigurationException.illegalConfiguration( 469 | "Error: The method '%s' does not have a user message defined.", 470 | method.name, 471 | ) 472 | } 473 | 474 | private fun findUserMessageTemplateFromMethodAnnotation(method: Method): Optional = 475 | Optional 476 | .ofNullable( 477 | method.getAnnotation( 478 | dev.langchain4j.service.UserMessage::class.java, 479 | ), 480 | ).map { userMessage -> 481 | getTemplate( 482 | method, 483 | "User", 484 | userMessage.fromResource, 485 | userMessage.value, 486 | userMessage.delimiter, 487 | ) 488 | } 489 | 490 | private fun findUserMessageTemplateFromAnnotatedParameter( 491 | parameters: Array, 492 | args: Array, 493 | ): Optional { 494 | for (i in parameters.indices) { 495 | if (parameters[i].isAnnotationPresent( 496 | dev.langchain4j.service.UserMessage::class.java, 497 | ) 498 | ) { 499 | return Optional.ofNullable(asString(args[i])) 500 | } 501 | } 502 | return Optional.empty() 503 | } 504 | } 505 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/AiServicesExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.service.AiServiceContext 4 | import dev.langchain4j.service.AiServices 5 | import dev.langchain4j.spi.services.AiServicesFactory 6 | 7 | /** 8 | * Creates an [AiServices] instance using the provided [AiServicesFactory]. 9 | */ 10 | public fun createAiService( 11 | serviceClass: Class, 12 | factory: AiServicesFactory, 13 | ): AiServices = AiServiceContext(serviceClass).let { context -> factory.create(context) } 14 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/AsyncAiServices.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.service.AiServiceContext 4 | import dev.langchain4j.service.AiServices 5 | import dev.langchain4j.service.IllegalConfigurationException.illegalConfiguration 6 | import dev.langchain4j.service.MemoryId 7 | import dev.langchain4j.service.Moderate 8 | import dev.langchain4j.service.Result 9 | import dev.langchain4j.service.TypeUtils 10 | import dev.langchain4j.service.memory.ChatMemoryAccess 11 | import dev.langchain4j.service.output.ServiceOutputParser 12 | import dev.langchain4j.spi.ServiceHelper 13 | import dev.langchain4j.spi.services.TokenStreamAdapter 14 | 15 | public class AsyncAiServices( 16 | context: AiServiceContext, 17 | ) : AiServices(context) { 18 | private val serviceOutputParser = ServiceOutputParser() 19 | private val tokenStreamAdapters = 20 | ServiceHelper.loadFactories(TokenStreamAdapter::class.java) 21 | 22 | @Suppress("NestedBlockDepth") 23 | override fun build(): T { 24 | performBasicValidation() 25 | 26 | if (!context.hasChatMemory() && 27 | ChatMemoryAccess::class.java.isAssignableFrom(context.aiServiceClass) 28 | ) { 29 | throw illegalConfiguration( 30 | "In order to have a service implementing ChatMemoryAccess, " + 31 | "please configure the ChatMemoryProvider on the '%s'.", 32 | context.aiServiceClass.name, 33 | ) 34 | } 35 | 36 | for (method in context.aiServiceClass.methods) { 37 | if (method.isAnnotationPresent(Moderate::class.java) && 38 | context.moderationModel == null 39 | ) { 40 | throw illegalConfiguration( 41 | "The @Moderate annotation is present, but the moderationModel is not set up. " + 42 | "Please ensure a valid moderationModel is configured " + 43 | "before using the @Moderate annotation.", 44 | ) 45 | } 46 | if (method.returnType in 47 | arrayOf( 48 | // supported collection types 49 | Result::class.java, 50 | MutableList::class.java, 51 | List::class.java, 52 | MutableSet::class.java, 53 | ) 54 | ) { 55 | TypeUtils.validateReturnTypesAreProperlyParametrized( 56 | method.name, 57 | method.genericReturnType, 58 | ) 59 | } 60 | 61 | if (!context.hasChatMemory()) { 62 | for (parameter in method.parameters) { 63 | if (parameter.isAnnotationPresent(MemoryId::class.java)) { 64 | throw illegalConfiguration( 65 | "In order to use @MemoryId, please configure " + 66 | "ChatMemoryProvider on the '%s'.", 67 | context.aiServiceClass.name, 68 | ) 69 | } 70 | } 71 | } 72 | } 73 | 74 | val handler = AiServiceOrchestrator(context, serviceOutputParser, tokenStreamAdapters) 75 | @Suppress("UNCHECKED_CAST", "unused") 76 | return ReflectionHelper.createSuspendProxy(context.aiServiceClass) { method, args -> 77 | return@createSuspendProxy handler.execute(method, args) 78 | } as T 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/AsyncAiServicesFactory.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.service.AiServiceContext 4 | import dev.langchain4j.service.AiServices 5 | import dev.langchain4j.spi.services.AiServicesFactory 6 | 7 | public class AsyncAiServicesFactory : AiServicesFactory { 8 | override fun create(context: AiServiceContext): AiServices = 9 | AsyncAiServices(context) 10 | } 11 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/ReflectionHelper.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.service.IllegalConfigurationException 4 | import dev.langchain4j.service.MemoryId 5 | import dev.langchain4j.service.UserMessage 6 | import dev.langchain4j.service.UserName 7 | import dev.langchain4j.service.V 8 | import kotlinx.coroutines.DelicateCoroutinesApi 9 | import kotlinx.coroutines.runBlocking 10 | import me.kpavlov.langchain4j.kotlin.service.invoker.HybridVirtualThreadInvocationHandler 11 | import java.lang.reflect.Method 12 | import java.lang.reflect.ParameterizedType 13 | import java.lang.reflect.Proxy 14 | import java.lang.reflect.Type 15 | import java.lang.reflect.WildcardType 16 | import kotlin.coroutines.Continuation 17 | 18 | @OptIn(DelicateCoroutinesApi::class) 19 | internal object ReflectionHelper { 20 | fun validateParameters(method: Method) { 21 | val parameters = method.getParameters() 22 | if (parameters == null || parameters.size < 2) { 23 | return 24 | } 25 | 26 | for (parameter in parameters) { 27 | if ("$parameter".startsWith("kotlin.coroutines.Continuation")) { 28 | // skip continuation parameter 29 | continue 30 | } 31 | val v = parameter.getAnnotation(V::class.java) 32 | val userMessage = 33 | parameter.getAnnotation(UserMessage::class.java) 34 | val memoryId = parameter.getAnnotation(MemoryId::class.java) 35 | val userName = parameter.getAnnotation(UserName::class.java) 36 | @Suppress("ComplexCondition") 37 | if (v == null && userMessage == null && memoryId == null && userName == null) { 38 | throw IllegalConfigurationException.illegalConfiguration( 39 | "Parameter '%s' of method '%s' should be annotated with @V or @UserMessage " + 40 | "or @UserName or @MemoryId", 41 | parameter.getName(), 42 | method.getName(), 43 | ) 44 | } 45 | } 46 | } 47 | 48 | @Throws(kotlin.IllegalStateException::class) 49 | private fun getReturnType(method: Method): Type { 50 | val continuationParam = method.parameterTypes.findLast { it.kotlin is Continuation<*> } 51 | if (continuationParam != null) { 52 | @Suppress("UseCheckOrError") 53 | return continuationParam.genericInterfaces[0] 54 | ?: throw IllegalStateException( 55 | "Can't find generic interface of continuation parameter", 56 | ) 57 | } 58 | return method.getGenericReturnType() 59 | } 60 | 61 | fun getSuspendReturnType(method: Method): java.lang.reflect.Type { 62 | val parameters = method.genericParameterTypes 63 | if (parameters.isEmpty()) return getReturnType(method) 64 | val lastParameter = parameters.last() 65 | // Check if the last parameter is Continuation 66 | return ( 67 | if (lastParameter is ParameterizedType && 68 | (lastParameter.rawType as? Class<*>)?.name == Continuation::class.java.name 69 | ) { 70 | // T is the first (and only) type argument 71 | val type = lastParameter.actualTypeArguments.first() 72 | if (type is WildcardType) { 73 | type.lowerBounds.first() 74 | } else { 75 | type 76 | } 77 | } else { 78 | getReturnType(method) // Not a suspend function, or not detectably so 79 | } 80 | ) 81 | } 82 | 83 | @Suppress("UNCHECKED_CAST") 84 | fun createSuspendProxy( 85 | iface: Class, 86 | handler: suspend (method: java.lang.reflect.Method, args: Array) -> Any?, 87 | ): T { 88 | // Create a HybridVirtualThreadInvocationHandler that uses the provided handler 89 | // for both suspend and blocking operations 90 | val invocationHandler = 91 | HybridVirtualThreadInvocationHandler( 92 | executeSuspend = { method, args -> 93 | handler(method, args as Array) 94 | }, 95 | executeSync = { method, args -> 96 | // For blocking operations, we run the suspend handler in a blocking context 97 | runBlocking { 98 | handler(method, args as Array) 99 | } 100 | }, 101 | ) 102 | 103 | return Proxy.newProxyInstance( 104 | iface.classLoader, 105 | arrayOf(iface), 106 | invocationHandler, 107 | ) as T 108 | } 109 | 110 | internal fun dropContinuationArg(args: Array): Array = 111 | args 112 | .dropLastWhile { 113 | it is Continuation<*> 114 | }.toTypedArray() 115 | } 116 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/ReflectionVariableResolver.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.internal.Exceptions 4 | import dev.langchain4j.model.input.structured.StructuredPrompt 5 | import dev.langchain4j.model.input.structured.StructuredPromptProcessor 6 | import dev.langchain4j.service.InternalReflectionVariableResolver 7 | import dev.langchain4j.service.MemoryId 8 | import dev.langchain4j.service.UserName 9 | import me.kpavlov.langchain4j.kotlin.ChatMemoryId 10 | import java.lang.reflect.Method 11 | import java.lang.reflect.Parameter 12 | import java.util.Optional 13 | 14 | /** 15 | * Utility class responsible for resolving variable names and values for prompt templates 16 | * by leveraging method parameters and their annotations. 17 | * 18 | * 19 | * This class is intended for internal use only and is designed to extract and map 20 | * parameter values to template variables in methods defined within AI services. 21 | * 22 | * @see https://github.com/langchain4j/langchain4j/pull/2951 23 | */ 24 | internal object ReflectionVariableResolver { 25 | public fun findTemplateVariables( 26 | template: String, 27 | method: Method, 28 | args: Array?, 29 | ): MutableMap = 30 | InternalReflectionVariableResolver.findTemplateVariables(template, method, args) 31 | 32 | public fun asString(arg: Any?): String = 33 | if (arg == null) { 34 | "null" 35 | } else if (arg is Array<*>?) { 36 | arrayAsString(arg) 37 | } else if (arg.javaClass.isAnnotationPresent(StructuredPrompt::class.java)) { 38 | StructuredPromptProcessor.toPrompt(arg).text() 39 | } else { 40 | arg.toString() 41 | } 42 | 43 | private fun arrayAsString(arg: Array<*>?): String = 44 | if (arg == null) { 45 | "null" 46 | } else { 47 | val sb = StringBuilder("[") 48 | val length = arg.size 49 | for (i in 0..?, 61 | args: Array, 62 | ): Optional = 63 | if ( 64 | parameters != null && 65 | parameters.size == 1 && 66 | parameters[0].getAnnotations().size == 0 67 | ) { 68 | Optional.ofNullable(asString(args[0])) 69 | } else { 70 | Optional.empty() 71 | } 72 | 73 | 74 | fun findUserName( 75 | parameters: Array, 76 | args: Array, 77 | ): Optional { 78 | var result = Optional.empty() 79 | for (i in args.indices) { 80 | if (parameters[i].isAnnotationPresent(UserName::class.java)) { 81 | result = Optional.of(args[i].toString()) 82 | break 83 | } 84 | } 85 | return result 86 | } 87 | 88 | @Suppress("ReturnCount") 89 | fun findMemoryId( 90 | method: Method, 91 | args: Array?, 92 | ): Optional { 93 | if (args == null) { 94 | return Optional.empty() 95 | } 96 | 97 | val memoryIdParam = findMemoryIdParameter(method, args) 98 | if (memoryIdParam != null) { 99 | val (parameter, memoryId) = memoryIdParam 100 | if (memoryId is ChatMemoryId) { 101 | return Optional.of(memoryId) 102 | } else { 103 | throw Exceptions.illegalArgument( 104 | "The value of parameter '%s' annotated with @MemoryId in method '%s' must not be null", 105 | parameter.getName(), 106 | method.getName(), 107 | ) 108 | } 109 | } 110 | 111 | return Optional.empty() 112 | } 113 | 114 | private fun findMemoryIdParameter(method: Method, args: Array): Pair? { 115 | for (i in args.indices) { 116 | val parameter = method.parameters[i] 117 | if (parameter.isAnnotationPresent(MemoryId::class.java)) { 118 | return Pair(parameter, args[i]) 119 | } 120 | } 121 | return null 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/SystemMessageProvider.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import me.kpavlov.langchain4j.kotlin.ChatMemoryId 4 | import me.kpavlov.langchain4j.kotlin.PromptContent 5 | import java.util.function.Function 6 | 7 | /** 8 | * Interface for providing LLM system messages based on a given chat memory identifier. 9 | */ 10 | @FunctionalInterface 11 | public interface SystemMessageProvider : Function { 12 | /** 13 | * Provides a system message based on the given chat memory identifier. 14 | * 15 | * @param chatMemoryID Identifier for the chat memory used to generate the system message. 16 | * @return A system prompt string associated with the provided chat memory identifier, maybe `null` 17 | */ 18 | public fun getSystemMessage(chatMemoryID: ChatMemoryId): PromptContent? 19 | 20 | /** 21 | * Applies the given chat memory identifier to generate the corresponding system message. 22 | * 23 | * @param chatMemoryID The identifier representing the chat memory to be used. 24 | * @return The prompt content associated with the specified chat memory identifier, 25 | * or `null` if no system message is available. 26 | */ 27 | public override fun apply(chatMemoryID: ChatMemoryId): PromptContent? = 28 | getSystemMessage(chatMemoryID) 29 | } 30 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/TemplateSystemMessageProvider.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import me.kpavlov.langchain4j.kotlin.ChatMemoryId 4 | import me.kpavlov.langchain4j.kotlin.Configuration 5 | import me.kpavlov.langchain4j.kotlin.PromptContent 6 | import me.kpavlov.langchain4j.kotlin.TemplateName 7 | import me.kpavlov.langchain4j.kotlin.prompt.PromptTemplateSource 8 | import me.kpavlov.langchain4j.kotlin.prompt.TemplateRenderer 9 | 10 | /** 11 | * TemplateSystemMessageProvider is responsible for providing system messages based on templates. 12 | * 13 | * @property templateName The name of the template to use for generating system messages. 14 | * @property promptTemplateSource Source from which the prompt templates are fetched. 15 | * @property promptTemplateRenderer Renderer used to render the content with specific variables. 16 | */ 17 | public open class TemplateSystemMessageProvider( 18 | private val templateName: TemplateName, 19 | private val promptTemplateSource: PromptTemplateSource = Configuration.promptTemplateSource, 20 | private val promptTemplateRenderer: TemplateRenderer = Configuration.promptTemplateRenderer, 21 | ) : SystemMessageProvider { 22 | public open fun templateName(): TemplateName = templateName 23 | 24 | public constructor( 25 | templateName: TemplateName, 26 | ) : this( 27 | templateName = templateName, 28 | promptTemplateSource = Configuration.promptTemplateSource, 29 | promptTemplateRenderer = Configuration.promptTemplateRenderer, 30 | ) 31 | 32 | /** 33 | * Generates a system message using a template and the provided chat memory identifier. 34 | * 35 | * @param chatMemoryID Identifier for the chat memory used to generate the system message. 36 | * @return A rendered prompt content string based on the template and chat memory identifier, 37 | * or `null` if the template is not found. 38 | */ 39 | override fun getSystemMessage(chatMemoryID: ChatMemoryId): PromptContent? { 40 | val promptTemplate = promptTemplateSource.getTemplate(templateName()) 41 | require(promptTemplate != null) { 42 | "Can't find SystemPrompt template with name=\"$templateName()\"" 43 | } 44 | val content = promptTemplate.content() 45 | return promptTemplateRenderer.render(content, mapOf("chatMemoryID" to chatMemoryID)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/TokenStreamExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.service.TokenStream 4 | import kotlinx.coroutines.channels.Channel 5 | import kotlinx.coroutines.channels.awaitClose 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.buffer 8 | import kotlinx.coroutines.flow.callbackFlow 9 | import kotlinx.coroutines.flow.flow 10 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply 11 | 12 | public fun TokenStream.asFlow(): Flow = 13 | flow { 14 | callbackFlow { 15 | onPartialResponse { trySend(it) } 16 | onCompleteResponse { close() } 17 | onError { close(it) } 18 | start() 19 | awaitClose() 20 | }.buffer(Channel.UNLIMITED).collect(this) 21 | } 22 | 23 | public fun TokenStream.asReplyFlow(): Flow = 24 | flow { 25 | callbackFlow { 26 | onPartialResponse { token -> 27 | trySend(StreamingChatModelReply.PartialResponse(token)) 28 | } 29 | onCompleteResponse { response -> 30 | trySend(StreamingChatModelReply.CompleteResponse(response)) 31 | close() 32 | } 33 | onError { throwable -> close(throwable) } 34 | start() 35 | awaitClose() 36 | }.buffer(Channel.UNLIMITED).collect(this) 37 | } 38 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/invoker/AiServiceOrchestrator.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.invoker 2 | 3 | import kotlinx.coroutines.delay 4 | import java.lang.reflect.Method 5 | import kotlin.reflect.KClass 6 | import kotlin.time.Duration.Companion.milliseconds 7 | 8 | /** 9 | * A simple service orchestrator that executes methods on a service. 10 | * This is a placeholder implementation that returns the parameters as a string. 11 | */ 12 | internal class AiServiceOrchestrator( 13 | @Suppress("UNUSED_PARAMETER") private val serviceClass: KClass, 14 | ) { 15 | suspend fun execute( 16 | @Suppress("UNUSED_PARAMETER") method: Method, 17 | params: Map, 18 | ): R? { 19 | delay(5.milliseconds) 20 | @Suppress("UNCHECKED_CAST") 21 | return params.toString() as? R? 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/invoker/HybridVirtualThreadInvocationHandler.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.invoker 2 | 3 | import dev.langchain4j.internal.VirtualThreadUtils 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.asCoroutineDispatcher 6 | import kotlinx.coroutines.future.future 7 | import kotlinx.coroutines.launch 8 | import kotlinx.coroutines.withContext 9 | import org.jetbrains.annotations.Blocking 10 | import java.lang.reflect.InvocationHandler 11 | import java.lang.reflect.Method 12 | import java.util.concurrent.CompletableFuture 13 | import java.util.concurrent.CompletionStage 14 | import java.util.concurrent.Executor 15 | import java.util.concurrent.Executors 16 | import kotlin.coroutines.Continuation 17 | 18 | public class HybridVirtualThreadInvocationHandler( 19 | private val virtualThreadExecutor: Executor = 20 | VirtualThreadUtils.createVirtualThreadExecutor { 21 | Executors.newCachedThreadPool() 22 | }!!, 23 | private val scope: CoroutineScope = 24 | CoroutineScope(virtualThreadExecutor.asCoroutineDispatcher()), 25 | private val executeSuspend: suspend (method: Method, args: Array?) -> Any?, 26 | private val executeSync: (method: Method, args: Array?) -> Any?, 27 | ) : InvocationHandler { 28 | // Create a dispatcher backed by virtual threads (requires Java 21+) 29 | private val virtualThreadDispatcher = 30 | virtualThreadExecutor.asCoroutineDispatcher() 31 | 32 | override fun invoke( 33 | proxy: Any, 34 | method: Method, 35 | args: Array?, 36 | ): Any? { 37 | // Check if the method is suspended (last parameter is Continuation) 38 | val isSuspend = 39 | method.parameterTypes.lastOrNull()?.let { 40 | Continuation::class.java.isAssignableFrom(it) 41 | } ?: false 42 | 43 | // Check if method is annotated with @Blocking 44 | val isBlocking = method.isAnnotationPresent(Blocking::class.java) 45 | 46 | return when { 47 | isSuspend -> { 48 | // Handle suspend function (return Unit, actual result goes to continuation) 49 | @Suppress("UNCHECKED_CAST") 50 | val continuation = args?.last() as? Continuation 51 | val actualArgs = args?.dropLast(1)?.toTypedArray() 52 | 53 | scope.launch { 54 | @Suppress("TooGenericExceptionCaught") 55 | try { 56 | // Execute the method, using virtual thread dispatcher if blocking 57 | val result = 58 | if (isBlocking) { 59 | withContext(virtualThreadDispatcher) { 60 | executeSuspend(method, actualArgs) 61 | } 62 | } else { 63 | executeSuspend(method, actualArgs) 64 | } 65 | continuation?.resumeWith(Result.success(result)) 66 | } catch (e: Exception) { 67 | continuation?.resumeWith(Result.failure(e)) 68 | } 69 | } 70 | 71 | kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED 72 | } 73 | // Handle CompletableFuture or CompletionStage 74 | CompletionStage::class.java.isAssignableFrom(method.returnType) -> { 75 | scope.future { 76 | if (isBlocking) { 77 | withContext(virtualThreadDispatcher) { 78 | executeSuspend(method, args) 79 | } 80 | } else { 81 | executeSuspend(method, args) 82 | } 83 | } 84 | } 85 | // Handle regular synchronous methods 86 | else -> { 87 | // For synchronous methods, run on virtual thread if blocking 88 | if (isBlocking) { 89 | // Run blocking operation on virtual thread and wait for result 90 | CompletableFuture 91 | .supplyAsync( 92 | { executeSync(method, args) }, 93 | virtualThreadExecutor, 94 | ).join() 95 | } else { 96 | // For regular non-blocking synchronous methods 97 | executeSync(method, args) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/invoker/KServices.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.invoker 2 | 3 | import kotlinx.coroutines.DelicateCoroutinesApi 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.GlobalScope 6 | import kotlinx.coroutines.launch 7 | import kotlinx.coroutines.runBlocking 8 | import org.slf4j.LoggerFactory 9 | import java.lang.reflect.Method 10 | import java.lang.reflect.ParameterizedType 11 | import java.lang.reflect.Proxy 12 | import java.lang.reflect.Type 13 | import java.util.concurrent.CompletionStage 14 | import kotlin.coroutines.Continuation 15 | import kotlin.reflect.KClass 16 | 17 | /** 18 | * Utility class for creating dynamic proxies for AI services. 19 | */ 20 | internal object KServices { 21 | private val logger = LoggerFactory.getLogger(KServices::class.java) 22 | 23 | /** 24 | * Checks if the current thread is a virtual thread. 25 | * 26 | * @return true if the current thread is a virtual thread, false otherwise 27 | */ 28 | private fun isVirtualThread(): Boolean { 29 | // Use reflection to check if Thread.currentThread().isVirtual() exists and call it 30 | return try { 31 | val isVirtualMethod = Thread::class.java.getMethod("isVirtual") 32 | isVirtualMethod.invoke(Thread.currentThread()) as Boolean 33 | } catch (_: Exception) { 34 | // If the method doesn't exist or fails, assume it's not a virtual thread 35 | false 36 | } 37 | } 38 | 39 | /** 40 | * Ensures that the current thread is a virtual thread. 41 | * 42 | * @throws IllegalStateException if the current thread is not a virtual thread 43 | */ 44 | private fun ensureVirtualThread() { 45 | check(isVirtualThread()) { 46 | "Synchronous methods must be executed in a virtual thread. " + 47 | "Use Executors.newVirtualThreadPerTaskExecutor() " + 48 | "or similar to create virtual threads." 49 | } 50 | } 51 | 52 | /** 53 | * Creates a dynamic proxy for the given service class. 54 | * 55 | * @param serviceClass The service class to create a proxy for 56 | * @return A proxy instance of the service class 57 | */ 58 | @OptIn(DelicateCoroutinesApi::class) 59 | inline fun create(serviceClass: KClass): T { 60 | ServiceClassValidator.validateClass(serviceClass) 61 | 62 | val executor = AiServiceOrchestrator(serviceClass) 63 | 64 | // Create a HybridVirtualThreadInvocationHandler that handles both suspend and blocking operations 65 | val handler = 66 | HybridVirtualThreadInvocationHandler( 67 | // Handle suspends functions 68 | executeSuspend = { method, args -> 69 | logger.debug("Executing suspend method: {}", method) 70 | // Extract parameters from args 71 | val params = extractParameters(method, args) 72 | 73 | // Execute the method using the executor 74 | executor.execute(method, params) 75 | }, 76 | // Handle blocking functions 77 | executeSync = { method, args -> 78 | logger.debug("Executing sync method: {}", method) 79 | // Handle Object methods 80 | when { 81 | method.name == "toString" -> { 82 | return@HybridVirtualThreadInvocationHandler "Proxy for $serviceClass" 83 | } 84 | 85 | method.declaringClass == Any::class.java -> { 86 | return@HybridVirtualThreadInvocationHandler method.invoke(this, args) 87 | } 88 | } 89 | 90 | // Extract parameters from args 91 | val params = extractParameters(method, args) 92 | 93 | // For void methods, launch a coroutine and return null 94 | if (method.returnType == Void.TYPE) { 95 | GlobalScope.launch(Dispatchers.IO) { 96 | executor.execute(method, params) 97 | } 98 | return@HybridVirtualThreadInvocationHandler null 99 | } 100 | 101 | // For other methods, ensure we're running in a virtual thread and execute synchronously 102 | ensureVirtualThread() 103 | 104 | runBlocking { 105 | executor.execute(method, params) 106 | } 107 | }, 108 | ) 109 | 110 | @Suppress("UNCHECKED_CAST") 111 | return Proxy.newProxyInstance( 112 | T::class.java.classLoader, 113 | arrayOf(T::class.java), 114 | handler, 115 | ) as T 116 | } 117 | 118 | /** 119 | * Extracts parameters from method arguments. 120 | */ 121 | private fun extractParameters( 122 | method: Method, 123 | args: Array?, 124 | ): Map { 125 | val params = mutableMapOf() 126 | 127 | if (args != null) { 128 | for (i in args.indices) { 129 | val arg = args[i] 130 | if (arg is Continuation<*>) continue 131 | val paramName = method.parameters[i].name ?: "param$i" 132 | params[paramName] = arg 133 | } 134 | } 135 | 136 | return params 137 | } 138 | 139 | /** 140 | * Gets the return type of a method, handling both regular and suspend methods. 141 | */ 142 | @Suppress("unused", "ReturnCount") 143 | private fun getReturnType(method: Method): Type { 144 | // For suspend functions, check if the last parameter is a Continuation 145 | val parameters = method.genericParameterTypes 146 | if (parameters.isNotEmpty()) { 147 | val lastParam = parameters.last() 148 | if (lastParam is ParameterizedType && 149 | (lastParam.rawType as? Class<*>)?.name == Continuation::class.java.name 150 | ) { 151 | // Return the type argument of the Continuation 152 | return lastParam.actualTypeArguments[0] 153 | } 154 | } 155 | 156 | // For regular functions or functions returning CompletionStage/CompletableFuture 157 | val returnType = method.genericReturnType 158 | 159 | // If it's a CompletionStage or CompletableFuture, return its type parameter 160 | if (returnType is ParameterizedType && 161 | CompletionStage::class.java.isAssignableFrom(returnType.rawType as Class<*>) 162 | ) { 163 | return returnType.actualTypeArguments[0] 164 | } 165 | 166 | return returnType 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/invoker/ServiceClassValidator.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.invoker 2 | 3 | import org.slf4j.LoggerFactory 4 | import kotlin.reflect.KClass 5 | import kotlin.reflect.KFunction 6 | 7 | /** 8 | * Utility for validating AiServices interface and their methods 9 | * at the time of AiService proxy construction. 10 | */ 11 | public object ServiceClassValidator { 12 | private val logger = LoggerFactory.getLogger(ServiceClassValidator::class.java) 13 | 14 | public fun validateClass(serviceClass: KClass) { 15 | serviceClass.members 16 | .filter { it is KFunction } 17 | .map { it as KFunction } 18 | .forEach { method -> 19 | logger.info( 20 | "Discovered method: {} ({})${method.name} : {}", 21 | method.name, 22 | method.parameters, 23 | method.returnType, 24 | ) 25 | validateMethod(method) 26 | } 27 | } 28 | 29 | public fun validateMethod(method: KFunction<*>) { 30 | logger.debug("Validating method: {}", method) 31 | // real validation 32 | logger.debug("Method: {} is good", method) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/service/memory/ChatMemoryServiceExtensions.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.memory 2 | 3 | import dev.langchain4j.memory.ChatMemory 4 | import dev.langchain4j.service.memory.ChatMemoryService 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | import me.kpavlov.langchain4j.kotlin.ChatMemoryId 8 | import kotlin.coroutines.CoroutineContext 9 | 10 | public suspend fun ChatMemoryService.getOrCreateChatMemoryAsync( 11 | memoryId: ChatMemoryId, 12 | context: CoroutineContext = Dispatchers.IO, 13 | ): ChatMemory = 14 | withContext(context) { this@getOrCreateChatMemoryAsync.getOrCreateChatMemory(memoryId) } 15 | 16 | public suspend fun ChatMemoryService.getChatMemoryAsync( 17 | memoryId: ChatMemoryId, 18 | context: CoroutineContext = Dispatchers.IO, 19 | ): ChatMemory? = 20 | withContext(context) 21 | { this@getChatMemoryAsync.getChatMemory(memoryId) } 22 | 23 | public suspend fun ChatMemoryService.evictChatMemoryAsync( 24 | memoryId: ChatMemoryId, 25 | context: CoroutineContext = Dispatchers.IO, 26 | ): ChatMemory? = 27 | withContext(context) { this@evictChatMemoryAsync.evictChatMemory(memoryId) } 28 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/resources/META-INF/services/dev.langchain4j.spi.prompt.PromptTemplateFactory: -------------------------------------------------------------------------------- 1 | me.kpavlov.langchain4j.kotlin.prompt.PromptTemplateFactory 2 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/resources/META-INF/services/dev.langchain4j.spi.services.AiServicesFactory: -------------------------------------------------------------------------------- 1 | me.kpavlov.langchain4j.kotlin.service.AsyncAiServicesFactory 2 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/resources/META-INF/services/dev.langchain4j.spi.services.TokenStreamAdapter: -------------------------------------------------------------------------------- 1 | me.kpavlov.langchain4j.kotlin.model.adapters.TokenStreamToStringFlowAdapter 2 | me.kpavlov.langchain4j.kotlin.model.adapters.TokenStreamToReplyFlowAdapter 3 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/main/resources/langchain4j-kotlin.properties: -------------------------------------------------------------------------------- 1 | prompt.template.source=me.kpavlov.langchain4j.kotlin.prompt.ClasspathPromptTemplateSource 2 | prompt.template.renderer=me.kpavlov.langchain4j.kotlin.prompt.SimpleTemplateRenderer 3 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatModelTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.containsExactly 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isSameInstanceAs 7 | import dev.langchain4j.data.message.SystemMessage 8 | import dev.langchain4j.data.message.UserMessage 9 | import dev.langchain4j.model.chat.ChatModel 10 | import dev.langchain4j.model.chat.request.ChatRequest 11 | import dev.langchain4j.model.chat.response.ChatResponse 12 | import kotlinx.coroutines.test.runTest 13 | import me.kpavlov.langchain4j.kotlin.model.chat.chatAsync 14 | import org.junit.jupiter.api.Test 15 | import org.junit.jupiter.api.extension.ExtendWith 16 | import org.mockito.Captor 17 | import org.mockito.Mock 18 | import org.mockito.junit.jupiter.MockitoExtension 19 | import org.mockito.kotlin.whenever 20 | 21 | @ExtendWith(MockitoExtension::class) 22 | internal class ChatModelTest { 23 | @Mock 24 | lateinit var model: ChatModel 25 | 26 | @Captor 27 | lateinit var chatRequestCaptor: org.mockito.ArgumentCaptor 28 | 29 | @Mock 30 | lateinit var chatResponse: ChatResponse 31 | 32 | @Test 33 | fun `Should call chatAsync`() { 34 | val temp = 0.8 35 | runTest { 36 | whenever(model.chat(chatRequestCaptor.capture())).thenReturn(chatResponse) 37 | val systemMessage = SystemMessage.from("You are a helpful assistant") 38 | val userMessage = UserMessage.from("Say Hello") 39 | val response = 40 | model.chatAsync { 41 | messages += systemMessage 42 | messages += userMessage 43 | parameters { 44 | temperature = temp 45 | } 46 | } 47 | assertThat(response).isSameInstanceAs(chatResponse) 48 | val request = chatRequestCaptor.value 49 | assertThat(request.messages()).containsExactly(systemMessage, userMessage) 50 | with(request.parameters()) { 51 | assertThat(temperature()).isEqualTo(temp) 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/Documents.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin 2 | 3 | import dev.langchain4j.data.document.Document 4 | import dev.langchain4j.data.document.parser.TextDocumentParser 5 | import dev.langchain4j.data.document.source.FileSystemSource 6 | import me.kpavlov.langchain4j.kotlin.data.document.loadAsync 7 | import org.slf4j.Logger 8 | import java.nio.file.Paths 9 | 10 | public suspend fun loadDocument( 11 | documentName: String, 12 | logger: Logger, 13 | ): Document { 14 | val source = FileSystemSource(Paths.get("./src/test/resources/data/$documentName")) 15 | val document = loadAsync(source, TextDocumentParser()) 16 | 17 | with(document) { 18 | logger.info("Document Metadata: {}", metadata()) 19 | logger.info("Document Text: {}", text()) 20 | } 21 | return document 22 | } 23 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/TestEnvironment.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin 2 | 3 | import me.kpavlov.aimocks.openai.MockOpenai 4 | 5 | internal object TestEnvironment : me.kpavlov.finchly.BaseTestEnvironment( 6 | dotEnvFileDir = "../", 7 | ) { 8 | val openaiApiKey = get("OPENAI_API_KEY", "demo") 9 | val mockOpenAi = MockOpenai() 10 | } 11 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/adapters/ServiceWithFlowTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.adapters 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.hasSize 5 | import assertk.assertions.startsWith 6 | import dev.langchain4j.data.message.AiMessage 7 | import dev.langchain4j.model.chat.StreamingChatModel 8 | import dev.langchain4j.model.chat.request.ChatRequest 9 | import dev.langchain4j.model.chat.response.ChatResponse 10 | import dev.langchain4j.model.chat.response.StreamingChatResponseHandler 11 | import dev.langchain4j.service.AiServices 12 | import dev.langchain4j.service.UserName 13 | import dev.langchain4j.service.V 14 | import io.kotest.matchers.collections.shouldContainExactly 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.catch 17 | import kotlinx.coroutines.flow.toList 18 | import kotlinx.coroutines.test.runTest 19 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply 20 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply.CompleteResponse 21 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply.PartialResponse 22 | import org.junit.jupiter.api.Assertions.assertTrue 23 | import org.junit.jupiter.api.Test 24 | import org.junit.jupiter.api.extension.ExtendWith 25 | import org.mockito.Mock 26 | import org.mockito.junit.jupiter.MockitoExtension 27 | import org.mockito.kotlin.any 28 | import org.mockito.kotlin.doAnswer 29 | import org.mockito.kotlin.whenever 30 | 31 | @ExtendWith(MockitoExtension::class) 32 | internal class ServiceWithFlowTest { 33 | @Mock 34 | private lateinit var model: StreamingChatModel 35 | 36 | @Test 37 | fun `Should use TokenStreamToStringFlowAdapter`() = 38 | runTest { 39 | val partialToken1 = "Hello" 40 | val partialToken2 = "world" 41 | val completeResponse = ChatResponse.builder().aiMessage(AiMessage("Hello")).build() 42 | 43 | doAnswer { 44 | val handler = it.arguments[1] as StreamingChatResponseHandler 45 | handler.onPartialResponse(partialToken1) 46 | handler.onPartialResponse(partialToken2) 47 | handler.onCompleteResponse(completeResponse) 48 | }.whenever(model).chat(any(), any()) 49 | 50 | val assistant = 51 | AiServices 52 | .builder(Assistant::class.java) 53 | .streamingChatModel(model) 54 | .build() 55 | 56 | val result = 57 | assistant 58 | .askQuestion(userName = "My friend", question = "How are you?") 59 | .toList() 60 | 61 | result shouldContainExactly listOf(partialToken1, partialToken2) 62 | } 63 | 64 | @Test 65 | fun `Should use TokenStreamToStringFlowAdapter error`() = 66 | runTest { 67 | val partialToken1 = "Hello" 68 | val partialToken2 = "world" 69 | val error = RuntimeException("Test error") 70 | 71 | doAnswer { 72 | val handler = it.arguments[1] as StreamingChatResponseHandler 73 | handler.onPartialResponse(partialToken1) 74 | handler.onPartialResponse(partialToken2) 75 | handler.onError(error) 76 | }.whenever(model).chat(any(), any()) 77 | 78 | val assistant = 79 | AiServices 80 | .builder(Assistant::class.java) 81 | .streamingChatModel(model) 82 | .build() 83 | 84 | val response = 85 | assistant 86 | .askQuestion(userName = "My friend", question = "How are you?") 87 | .catch { 88 | val message = 89 | requireNotNull( 90 | it.message, 91 | ) { "Only $error is allowed to occur here but found $it" } 92 | emit(message) 93 | }.toList() 94 | 95 | response shouldContainExactly listOf(partialToken1, partialToken2, error.message) 96 | } 97 | 98 | @Test 99 | fun `Should use TokenStreamToReplyFlowAdapter`() = 100 | runTest { 101 | val partialToken1 = "Hello" 102 | val partialToken2 = "world" 103 | val completeResponse = ChatResponse.builder().aiMessage(AiMessage("Hello")).build() 104 | 105 | doAnswer { 106 | val handler = it.arguments[1] as StreamingChatResponseHandler 107 | handler.onPartialResponse(partialToken1) 108 | handler.onPartialResponse(partialToken2) 109 | handler.onCompleteResponse(completeResponse) 110 | }.whenever(model).chat(any(), any()) 111 | 112 | val assistant = 113 | AiServices 114 | .builder(Assistant::class.java) 115 | .streamingChatModel(model) 116 | .build() 117 | 118 | val result = 119 | assistant 120 | .askQuestion2(userName = "My friend", question = "How are you?") 121 | .toList() 122 | 123 | assertThat( 124 | result, 125 | ).startsWith(PartialResponse(partialToken1), PartialResponse(partialToken2)) 126 | assertTrue(result[2] is CompleteResponse) 127 | } 128 | 129 | @Test 130 | fun `Should use TokenStreamToReplyFlowAdapter error`() = 131 | runTest { 132 | val partialToken1 = "Hello" 133 | val partialToken2 = "world" 134 | val error = RuntimeException("Test error") 135 | 136 | doAnswer { 137 | val handler = it.arguments[1] as StreamingChatResponseHandler 138 | handler.onPartialResponse(partialToken1) 139 | handler.onPartialResponse(partialToken2) 140 | handler.onError(error) 141 | }.whenever(model).chat(any(), any()) 142 | 143 | val assistant = 144 | AiServices 145 | .builder(Assistant::class.java) 146 | .streamingChatModel(model) 147 | .build() 148 | 149 | val response = 150 | assistant 151 | .askQuestion2(userName = "My friend", question = "How are you?") 152 | .catch { emit(StreamingChatModelReply.Error(it)) } 153 | .toList() 154 | 155 | assertThat(response).hasSize(3) 156 | assertThat( 157 | response, 158 | ).startsWith(PartialResponse(partialToken1), PartialResponse(partialToken2)) 159 | assertTrue(response[2] is StreamingChatModelReply.Error) 160 | } 161 | 162 | @Suppress("unused") 163 | private interface Assistant { 164 | @dev.langchain4j.service.UserMessage( 165 | "Hello, I am {{ userName }}. {{ message }}.", 166 | ) 167 | fun askQuestion( 168 | @UserName userName: String, 169 | @V("message") question: String, 170 | ): Flow 171 | 172 | @dev.langchain4j.service.UserMessage( 173 | "Hello, I am {{ userName }}. {{ message }}.", 174 | ) 175 | fun askQuestion2( 176 | @UserName userName: String, 177 | @V("message") question: String, 178 | ): Flow 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/data/document/AsyncDocumentLoaderTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.data.document 2 | 3 | import dev.langchain4j.data.document.DocumentSource 4 | import dev.langchain4j.data.document.parser.TextDocumentParser 5 | import dev.langchain4j.data.document.source.FileSystemSource 6 | import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder 7 | import io.kotest.matchers.collections.shouldHaveSize 8 | import io.kotest.matchers.shouldNotBe 9 | import io.kotest.matchers.string.shouldContain 10 | import io.kotest.matchers.string.shouldNotBeEmpty 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.ExperimentalCoroutinesApi 13 | import kotlinx.coroutines.async 14 | import kotlinx.coroutines.awaitAll 15 | import kotlinx.coroutines.test.runTest 16 | import org.junit.jupiter.api.BeforeEach 17 | import org.junit.jupiter.api.Test 18 | import java.nio.file.Files 19 | import java.nio.file.Path 20 | import java.nio.file.Paths 21 | 22 | @OptIn(ExperimentalCoroutinesApi::class) 23 | internal class AsyncDocumentLoaderTest { 24 | private val logger = org.slf4j.LoggerFactory.getLogger(javaClass) 25 | private lateinit var documentSource: DocumentSource 26 | private val parser = TextDocumentParser() 27 | 28 | @BeforeEach 29 | fun beforeEach() { 30 | documentSource = 31 | FileSystemSource( 32 | Paths.get("./src/test/resources/data/books/captain-blood.txt"), 33 | ) 34 | } 35 | 36 | @Test 37 | fun `Should load documents asynchronously`() = 38 | runTest { 39 | val document = loadAsync(documentSource, parser) 40 | document.text() shouldContain "Captain Blood" 41 | document.metadata() shouldNotBe null 42 | } 43 | 44 | @Test 45 | fun `Should parse documents asynchronously`() = 46 | runTest { 47 | val document = parser.parseAsync(documentSource.inputStream()) 48 | document.text() shouldContain "Captain Blood" 49 | document.metadata() shouldNotBe null 50 | } 51 | 52 | @Test 53 | fun `Should load all documents asynchronously`() = 54 | runTest { 55 | println("AsyncDocumentLoaderTest.Should load all documents asynchronously") 56 | val rootPath = Paths.get("./src/test/resources/data") 57 | val paths = 58 | Files 59 | .walk(rootPath) 60 | .filter { Files.isRegularFile(it) } 61 | .toList() 62 | 63 | // Process each file in parallel 64 | val ioScope = Dispatchers.IO.limitedParallelism(10) 65 | val documents = 66 | paths 67 | .map { path -> 68 | async { 69 | try { 70 | loadAsync( 71 | source = FileSystemSource(path), 72 | parser = parser, 73 | context = ioScope, 74 | ) 75 | } catch (e: Exception) { 76 | logger.error("Failed to load document: $path", e) 77 | null 78 | } 79 | } 80 | }.awaitAll() 81 | .filterNotNull() 82 | 83 | documents.forEach { 84 | it.text().shouldNotBeEmpty() 85 | it.metadata() shouldNotBe null 86 | } 87 | } 88 | 89 | @Test 90 | fun `Should loadDocumentsAsync`() = 91 | runTest { 92 | val documents = 93 | loadDocumentsAsync( 94 | recursive = true, 95 | documentParser = parser, 96 | directoryPaths = listOf(Path.of("./src/test/resources/data")), 97 | ) 98 | documents shouldHaveSize 3 99 | 100 | val documentNames = documents.map { it.metadata().getString("file_name") } 101 | documentNames shouldContainExactlyInAnyOrder 102 | listOf( 103 | "captain-blood.txt", 104 | "quantum-computing.txt", 105 | "blumblefang.txt", 106 | ) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/data/document/DocumentParserExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.data.document 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.containsOnly 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isSameInstanceAs 7 | import dev.langchain4j.data.document.Document 8 | import dev.langchain4j.data.document.Document.ABSOLUTE_DIRECTORY_PATH 9 | import dev.langchain4j.data.document.Document.FILE_NAME 10 | import dev.langchain4j.data.document.DocumentParser 11 | import dev.langchain4j.data.document.DocumentSource 12 | import dev.langchain4j.data.document.Metadata 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.test.runTest 15 | import org.junit.jupiter.api.Test 16 | import org.junit.jupiter.api.extension.ExtendWith 17 | import org.mockito.Mock 18 | import org.mockito.junit.jupiter.MockitoExtension 19 | import org.mockito.kotlin.whenever 20 | 21 | @ExtendWith(MockitoExtension::class) 22 | internal class DocumentParserExtensionsKtTest { 23 | @Mock 24 | private lateinit var documentParser: DocumentParser 25 | 26 | @Mock 27 | private lateinit var documentSource: DocumentSource 28 | 29 | @Test 30 | fun `parseAsync should parse document and return it combined metadata`() { 31 | val documentContent = "parsed document content" 32 | runTest { 33 | val inputStream = documentContent.byteInputStream() 34 | val documentMetadata = Metadata.from(mapOf("title" to "Bar")) 35 | val fileMetadata = 36 | Metadata.from( 37 | mapOf( 38 | ABSOLUTE_DIRECTORY_PATH to "foo", 39 | FILE_NAME to "bar.txt", 40 | ), 41 | ) 42 | val document = Document.from(documentContent, documentMetadata) 43 | 44 | whenever(documentSource.inputStream()).thenReturn(inputStream) 45 | whenever(documentParser.parse(inputStream)).thenReturn(document) 46 | whenever(documentSource.metadata()).thenReturn(fileMetadata) 47 | 48 | val result = documentParser.parseAsync(documentSource, Dispatchers.IO) 49 | 50 | assertThat(result.text()).isEqualTo(documentContent) 51 | assertThat(result.metadata().toMap()) 52 | .containsOnly( 53 | ABSOLUTE_DIRECTORY_PATH to "foo", 54 | FILE_NAME to "bar.txt", 55 | "title" to "Bar", 56 | ) 57 | } 58 | } 59 | 60 | @Test 61 | fun `parseAsync should return parser document when no source metadata is present`() { 62 | val documentContent = "parsed document content" 63 | runTest { 64 | val inputStream = documentContent.byteInputStream() 65 | val documentMetadata = Metadata.from(mapOf("title" to "Bar")) 66 | val fileMetadata = Metadata.from(mapOf()) 67 | val document = Document.from(documentContent, documentMetadata) 68 | 69 | whenever(documentSource.inputStream()).thenReturn(inputStream) 70 | whenever(documentParser.parse(inputStream)).thenReturn(document) 71 | whenever(documentSource.metadata()).thenReturn(fileMetadata) 72 | 73 | val result = documentParser.parseAsync(documentSource, Dispatchers.IO) 74 | 75 | assertThat(result).isSameInstanceAs(document) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatModelExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import dev.langchain4j.model.chat.ChatModel 4 | import dev.langchain4j.model.chat.request.ChatRequest 5 | import dev.langchain4j.model.chat.response.ChatResponse 6 | import io.kotest.matchers.shouldBe 7 | import kotlinx.coroutines.test.runTest 8 | import org.junit.jupiter.api.Test 9 | import org.junit.jupiter.api.extension.ExtendWith 10 | import org.mockito.Mock 11 | import org.mockito.junit.jupiter.MockitoExtension 12 | import org.mockito.kotlin.verify 13 | import org.mockito.kotlin.whenever 14 | 15 | @ExtendWith(MockitoExtension::class) 16 | internal class ChatModelExtensionsKtTest { 17 | @Mock 18 | private lateinit var mockModel: ChatModel 19 | 20 | @Mock 21 | private lateinit var request: ChatRequest 22 | 23 | @Mock 24 | private lateinit var requestBuilder: ChatRequest.Builder 25 | 26 | @Mock 27 | private lateinit var expectedResponse: ChatResponse 28 | 29 | /** 30 | * This class tests the `chatAsync` extension function of `ChatModel`. 31 | * The function takes a `ChatRequest` or a `ChatRequest.Builder` as input, 32 | * performs asynchronous processing, and returns a `ChatResponse`. 33 | */ 34 | 35 | @Test 36 | fun `chatAsync should return expected ChatResponse when using ChatRequest`() = 37 | runTest { 38 | whenever(mockModel.chat(request)).thenReturn(expectedResponse) 39 | 40 | val actualResponse = mockModel.chatAsync(request) 41 | 42 | actualResponse shouldBe expectedResponse 43 | verify(mockModel).chat(request) 44 | } 45 | 46 | @Test 47 | fun `chatAsync should return expected ChatResponse when using ChatRequest Builder`() = 48 | runTest { 49 | whenever(requestBuilder.build()).thenReturn(request) 50 | whenever(mockModel.chat(request)).thenReturn(expectedResponse) 51 | 52 | val actualResponse = mockModel.chatAsync(requestBuilder) 53 | 54 | actualResponse shouldBe expectedResponse 55 | verify(mockModel).chat(request) 56 | } 57 | 58 | @Test 59 | fun `chat should return expected ChatResponse when using ChatRequest Builder`() = 60 | runTest { 61 | whenever(requestBuilder.build()).thenReturn(request) 62 | whenever(mockModel.chat(request)).thenReturn(expectedResponse) 63 | 64 | val actualResponse = mockModel.chat(requestBuilder) 65 | 66 | actualResponse shouldBe expectedResponse 67 | verify(mockModel).chat(request) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatModelIT.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.contains 5 | import assertk.assertions.isNotNull 6 | import dev.langchain4j.data.document.Document 7 | import dev.langchain4j.data.message.SystemMessage.systemMessage 8 | import dev.langchain4j.data.message.UserMessage.userMessage 9 | import dev.langchain4j.model.chat.ChatModel 10 | import dev.langchain4j.model.chat.request.ResponseFormat 11 | import dev.langchain4j.model.openai.OpenAiChatModel 12 | import kotlinx.coroutines.test.runTest 13 | import me.kpavlov.langchain4j.kotlin.TestEnvironment 14 | import me.kpavlov.langchain4j.kotlin.loadDocument 15 | import org.junit.jupiter.api.BeforeAll 16 | import org.junit.jupiter.api.Test 17 | import org.junit.jupiter.api.TestInstance 18 | import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable 19 | import org.slf4j.LoggerFactory 20 | 21 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 22 | @EnabledIfEnvironmentVariable( 23 | named = "OPENAI_API_KEY", 24 | matches = ".+", 25 | ) 26 | internal class ChatModelIT { 27 | private val logger = LoggerFactory.getLogger(javaClass) 28 | 29 | private val model: ChatModel = 30 | OpenAiChatModel 31 | .builder() 32 | .apiKey(TestEnvironment.openaiApiKey) 33 | .modelName("gpt-4o-mini") 34 | .temperature(0.0) 35 | .maxTokens(512) 36 | .build() 37 | 38 | private lateinit var document: Document 39 | 40 | @BeforeAll 41 | fun beforeAll() = 42 | runTest { 43 | document = loadDocument("notes/blumblefang.txt", logger) 44 | } 45 | 46 | @Test 47 | fun `ChatModel should chatAsync`() = 48 | runTest { 49 | val document = loadDocument("notes/blumblefang.txt", logger) 50 | 51 | val response = 52 | model.chatAsync { 53 | messages += 54 | systemMessage( 55 | """ 56 | You are helpful advisor answering questions only related to the given text 57 | """.trimIndent(), 58 | ) 59 | messages += 60 | userMessage( 61 | """ 62 | What does Blumblefang love? Text: ```${document.text()}``` 63 | """.trimIndent(), 64 | ) 65 | parameters { 66 | responseFormat = ResponseFormat.TEXT 67 | } 68 | } 69 | 70 | logger.info("Response: {}", response) 71 | assertThat(response).isNotNull() 72 | val content = response.aiMessage() 73 | assertThat(content.text()).contains("Blumblefang loves to help") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/StreamingChatModelExtensionsKtTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import dev.langchain4j.data.message.AiMessage 4 | import dev.langchain4j.data.message.AiMessage.aiMessage 5 | import dev.langchain4j.data.message.UserMessage.userMessage 6 | import dev.langchain4j.model.chat.StreamingChatModel 7 | import dev.langchain4j.model.chat.request.ChatRequest 8 | import dev.langchain4j.model.chat.response.ChatResponse 9 | import dev.langchain4j.model.chat.response.StreamingChatResponseHandler 10 | import io.kotest.assertions.throwables.shouldThrow 11 | import io.kotest.matchers.collections.shouldContainExactly 12 | import io.kotest.matchers.throwable.shouldHaveMessage 13 | import kotlinx.coroutines.channels.BufferOverflow 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.flow.onEach 16 | import kotlinx.coroutines.flow.toList 17 | import kotlinx.coroutines.test.runTest 18 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply.CompleteResponse 19 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply.PartialResponse 20 | import org.junit.jupiter.api.Test 21 | import org.junit.jupiter.api.extension.ExtendWith 22 | import org.mockito.Mock 23 | import org.mockito.junit.jupiter.MockitoExtension 24 | import org.mockito.kotlin.any 25 | import org.mockito.kotlin.doAnswer 26 | import org.mockito.kotlin.verify 27 | import org.mockito.kotlin.whenever 28 | 29 | @ExtendWith(MockitoExtension::class) 30 | internal class StreamingChatModelExtensionsKtTest { 31 | @Mock 32 | private lateinit var mockModel: StreamingChatModel 33 | 34 | @Test 35 | fun `chatFlow should handle partial and complete responses`() = 36 | runTest { 37 | val partialToken1 = "Hello" 38 | val partialToken2 = "world" 39 | val completeResponse = ChatResponse.builder().aiMessage(AiMessage("Hello")).build() 40 | 41 | // Simulate the streaming behavior with a mocked handler 42 | doAnswer { 43 | val handler = it.arguments[1] as StreamingChatResponseHandler 44 | handler.onPartialResponse(partialToken1) 45 | handler.onPartialResponse(partialToken2) 46 | handler.onCompleteResponse(completeResponse) 47 | }.whenever(mockModel).chat(any(), any()) 48 | 49 | val flow = 50 | mockModel.chatFlow { 51 | messages += userMessage("Hey, there!") 52 | } 53 | val result = flow.toList() 54 | 55 | // Assert partial responses 56 | result shouldContainExactly 57 | listOf( 58 | PartialResponse(partialToken1), 59 | PartialResponse(partialToken2), 60 | CompleteResponse(completeResponse), 61 | ) 62 | 63 | // Verify interactions 64 | verify(mockModel).chat(any(), any()) 65 | } 66 | 67 | @Test 68 | fun `chatFlow should respect buffering strategy`() = 69 | runTest { 70 | val partialToken0 = "start" 71 | val partialToken1 = "hello" 72 | val partialToken2 = "world" 73 | val completeResponse = 74 | ChatResponse 75 | .builder() 76 | .aiMessage(aiMessage("Done")) 77 | .build() 78 | 79 | // Simulate the streaming behavior with a mocked handler 80 | doAnswer { 81 | val handler = it.arguments[1] as StreamingChatResponseHandler 82 | handler.onPartialResponse(partialToken0) 83 | handler.onPartialResponse(partialToken1) 84 | handler.onPartialResponse(partialToken2) 85 | handler.onCompleteResponse(completeResponse) 86 | }.whenever(mockModel) 87 | .chat(any(), any()) 88 | 89 | val result = mutableListOf() 90 | mockModel 91 | .chatFlow( 92 | bufferCapacity = 1, 93 | onBufferOverflow = BufferOverflow.DROP_OLDEST, 94 | ) { 95 | messages += userMessage("Hey, there!") 96 | }.onEach { 97 | println(it) 98 | delay(100) 99 | }.collect { 100 | result.add(it) 101 | } 102 | 103 | // Assert partial responses 104 | result shouldContainExactly 105 | listOf( 106 | PartialResponse(partialToken0), 107 | CompleteResponse(completeResponse), 108 | ) 109 | 110 | // Verify interactions 111 | verify(mockModel) 112 | .chat(any(), any()) 113 | } 114 | 115 | @Test 116 | fun `chatFlow should handle errors`() = 117 | runTest { 118 | val error = RuntimeException("Test error") 119 | 120 | // Simulate the error during streaming 121 | doAnswer { 122 | val handler = it.arguments[1] as StreamingChatResponseHandler 123 | handler.onError(error) 124 | }.whenever(mockModel).chat(any(), any()) 125 | 126 | val flow = 127 | mockModel.chatFlow { 128 | messages += userMessage("Hey, there!") 129 | } 130 | 131 | val exception = 132 | shouldThrow { 133 | flow.toList() 134 | } 135 | 136 | exception shouldHaveMessage "Test error" 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/StreamingChatModelIT.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import dev.langchain4j.data.message.SystemMessage.systemMessage 4 | import dev.langchain4j.data.message.UserMessage.userMessage 5 | import dev.langchain4j.model.chat.StreamingChatModel 6 | import dev.langchain4j.model.chat.response.ChatResponse 7 | import io.kotest.matchers.nulls.shouldNotBeNull 8 | import io.kotest.matchers.string.shouldContain 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.flow.flow 11 | import kotlinx.coroutines.test.runTest 12 | import kotlinx.coroutines.yield 13 | import me.kpavlov.langchain4j.kotlin.TestEnvironment 14 | import me.kpavlov.langchain4j.kotlin.TestEnvironment.mockOpenAi 15 | import me.kpavlov.langchain4j.kotlin.loadDocument 16 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply.CompleteResponse 17 | import me.kpavlov.langchain4j.kotlin.model.chat.StreamingChatModelReply.PartialResponse 18 | import org.junit.jupiter.api.AfterEach 19 | import org.junit.jupiter.api.Assertions.fail 20 | import org.junit.jupiter.api.Test 21 | import org.slf4j.LoggerFactory 22 | import java.util.concurrent.ConcurrentLinkedQueue 23 | import java.util.concurrent.atomic.AtomicReference 24 | 25 | internal open class StreamingChatModelIT { 26 | private val logger = LoggerFactory.getLogger(javaClass) 27 | 28 | private val model: StreamingChatModel = createOpenAiStreamingModel() 29 | 30 | @AfterEach 31 | fun afterEach() { 32 | mockOpenAi.verifyNoUnmatchedRequests() 33 | } 34 | 35 | @Test 36 | fun `StreamingChatModel should generateFlow`() = 37 | runTest { 38 | val document = loadDocument("notes/blumblefang.txt", logger) 39 | 40 | val systemMessage = 41 | systemMessage( 42 | """ 43 | You are helpful advisor answering questions only related to the given text 44 | """.trimIndent(), 45 | ) 46 | val userMessage = 47 | userMessage( 48 | """ 49 | What does Blumblefang love? Text: ```${document.text()}``` 50 | """.trimIndent(), 51 | ) 52 | 53 | setupMockResponseIfNecessary( 54 | systemMessage.text(), 55 | "What does Blumblefang love", 56 | "Blumblefang loves to help and cookies", 57 | ) 58 | 59 | val responseRef = AtomicReference() 60 | 61 | val collectedTokens = ConcurrentLinkedQueue() 62 | 63 | model 64 | .chatFlow { 65 | messages += systemMessage 66 | messages += userMessage 67 | } 68 | .collect { 69 | when (it) { 70 | is PartialResponse -> { 71 | println("Token: '${it.token}'") 72 | collectedTokens += it.token 73 | } 74 | 75 | is CompleteResponse -> responseRef.set(it.response) 76 | is StreamingChatModelReply.Error -> fail("Error", it.cause) 77 | else -> fail("Unsupported event: $it") 78 | } 79 | } 80 | 81 | val response = responseRef.get()!! 82 | response.metadata().shouldNotBeNull() 83 | response.aiMessage().shouldNotBeNull { 84 | text() shouldContain "Blumblefang loves to help" 85 | } 86 | } 87 | 88 | fun setupMockResponseIfNecessary( 89 | expectedSystemMessage: String, 90 | expectedUserMessage: String, 91 | expectedAnswer: String, 92 | ) { 93 | if (TestEnvironment["OPENAI_API_KEY"] != null) { 94 | logger.error("Running with real OpenAI API") 95 | return 96 | } 97 | logger.error("Running with Mock OpenAI API (Ai-Mocks/Mokksy)") 98 | 99 | mockOpenAi.completion { 100 | requestBodyContains(expectedSystemMessage) 101 | requestBodyContains(expectedUserMessage) 102 | } respondsStream { 103 | responseFlow = 104 | flow { 105 | expectedAnswer.split(" ").forEach { token -> 106 | emit("$token ") 107 | yield() 108 | delay(42) 109 | } 110 | } 111 | sendDone = true 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/TestSetup.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat 2 | 3 | import dev.langchain4j.model.chat.StreamingChatModel 4 | import dev.langchain4j.model.openai.OpenAiStreamingChatModel 5 | import dev.langchain4j.model.openai.OpenAiStreamingChatModel.OpenAiStreamingChatModelBuilder 6 | import me.kpavlov.langchain4j.kotlin.TestEnvironment 7 | 8 | internal fun createOpenAiStreamingModel( 9 | configurer: OpenAiStreamingChatModelBuilder.() -> Unit = {}, 10 | ): StreamingChatModel { 11 | val modelBuilder = 12 | OpenAiStreamingChatModel 13 | .builder() 14 | .modelName("gpt-4o-mini") 15 | .temperature(0.1) 16 | .maxTokens(100) 17 | 18 | val apiKey = TestEnvironment["OPENAI_API_KEY"] 19 | if (apiKey != null) { 20 | modelBuilder.apiKey(apiKey) 21 | } else { 22 | modelBuilder 23 | .apiKey("my-key") 24 | .baseUrl(TestEnvironment.mockOpenAi.baseUrl()) 25 | } 26 | configurer.invoke(modelBuilder) 27 | 28 | return modelBuilder.build() 29 | } 30 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/request/ChatRequestExtensionsTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.model.chat.request 2 | 3 | import dev.langchain4j.agent.tool.ToolSpecification 4 | import dev.langchain4j.data.message.SystemMessage 5 | import dev.langchain4j.data.message.UserMessage 6 | import dev.langchain4j.model.chat.request.ChatRequestParameters 7 | import dev.langchain4j.model.chat.request.DefaultChatRequestParameters 8 | import dev.langchain4j.model.chat.request.ResponseFormat 9 | import dev.langchain4j.model.chat.request.ToolChoice 10 | import dev.langchain4j.model.openai.OpenAiChatRequestParameters 11 | import io.kotest.matchers.collections.shouldContainExactly 12 | import io.kotest.matchers.doubles.shouldBeWithinPercentageOf 13 | import io.kotest.matchers.should 14 | import io.kotest.matchers.shouldBe 15 | import io.kotest.matchers.shouldNotBe 16 | import io.kotest.matchers.types.beInstanceOf 17 | import org.junit.jupiter.api.Test 18 | import org.mockito.kotlin.mock 19 | 20 | internal class ChatRequestExtensionsTest { 21 | @Test 22 | fun `Should build ChatRequest`() { 23 | val systemMessage = SystemMessage("You are a helpful assistant") 24 | val userMessage = UserMessage("Send greeting") 25 | val params: ChatRequestParameters = mock() 26 | val result = 27 | chatRequest { 28 | messages += systemMessage 29 | messages += userMessage 30 | parameters { 31 | temperature = 0.1 32 | } 33 | parameters = params 34 | } 35 | 36 | result.messages() shouldContainExactly 37 | listOf( 38 | systemMessage, 39 | userMessage, 40 | ) 41 | result.parameters() shouldBe params 42 | result.parameters().temperature() shouldNotBe 0.1 43 | } 44 | 45 | @Test 46 | fun `Should build ChatRequest with parameters builder`() { 47 | val systemMessage = SystemMessage("You are a helpful assistant") 48 | val userMessage = UserMessage("Send greeting") 49 | val toolSpec: ToolSpecification = mock() 50 | val toolSpecs = listOf(toolSpec) 51 | val result = 52 | chatRequest { 53 | messages += systemMessage 54 | messages += userMessage 55 | parameters { 56 | temperature = 0.1 57 | modelName = "super-model" 58 | topP = 0.2 59 | topK = 3 60 | frequencyPenalty = 0.4 61 | presencePenalty = 0.5 62 | maxOutputTokens = 6 63 | stopSequences = listOf("halt", "stop") 64 | toolSpecifications = toolSpecs 65 | toolChoice = ToolChoice.REQUIRED 66 | responseFormat = ResponseFormat.JSON 67 | } 68 | } 69 | val parameters = result.parameters() 70 | parameters should beInstanceOf() 71 | parameters.temperature().shouldBeWithinPercentageOf(0.1, 0.000001) 72 | parameters.modelName() shouldBe "super-model" 73 | parameters.topP().shouldBeWithinPercentageOf(0.2, 0.000001) 74 | parameters.topK() shouldBe 3 75 | parameters.frequencyPenalty().shouldBeWithinPercentageOf(0.4, 0.000001) 76 | parameters.presencePenalty().shouldBeWithinPercentageOf(0.5, 0.000001) 77 | parameters.maxOutputTokens() shouldBe 6 78 | parameters.stopSequences() shouldContainExactly listOf("halt", "stop") 79 | parameters.toolSpecifications() shouldContainExactly listOf(toolSpec) 80 | parameters.toolChoice() shouldBe ToolChoice.REQUIRED 81 | parameters.responseFormat() shouldBe ResponseFormat.JSON 82 | } 83 | 84 | @Test 85 | fun `Should build ChatRequest with OpenAi parameters builder`() { 86 | val systemMessage = SystemMessage("You are a helpful assistant") 87 | val userMessage = UserMessage("Send greeting") 88 | val result = 89 | chatRequest { 90 | messages += systemMessage 91 | messages += userMessage 92 | parameters(OpenAiChatRequestParameters.builder()) { 93 | temperature = 0.1 94 | builder.seed(42) 95 | } 96 | } 97 | val parameters = result.parameters() as OpenAiChatRequestParameters 98 | parameters.temperature().shouldBeWithinPercentageOf(0.1, 0.000001) 99 | parameters.seed() shouldBe 42 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/prompt/SimpleTemplateRendererTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.prompt 2 | 3 | import org.junit.jupiter.api.Assertions.assertEquals 4 | import org.junit.jupiter.api.Test 5 | import org.junit.jupiter.api.assertThrows 6 | 7 | internal class SimpleTemplateRendererTest { 8 | private val subject = SimpleTemplateRenderer() 9 | 10 | @Test 11 | fun `render returns original string when no placeholders are present`() { 12 | val template = "No placeholders here" 13 | val variables: Map = mapOf("key" to "value") 14 | 15 | val result = subject.render(template, variables) 16 | 17 | assertEquals(template, result) 18 | } 19 | 20 | @Test 21 | fun `render replaces placeholders with variable values`() { 22 | val template = "Hello, {{name}}" 23 | val variables: Map = mapOf("name" to "John") 24 | 25 | val result = subject.render(template, variables) 26 | 27 | assertEquals("Hello, John", result) 28 | } 29 | 30 | @Test 31 | fun `render replaces multiple placeholders with variable values`() { 32 | val template = "Hello, {{name}}, you are {{age}} years old." 33 | val variables: Map = mapOf("name" to "John", "age" to 47) 34 | 35 | val result = subject.render(template, variables) 36 | 37 | assertEquals("Hello, John, you are 47 years old.", result) 38 | } 39 | 40 | @Test 41 | fun `render replaces undefined placeholders with an empty string`() { 42 | val template = "Hello, {{customer}}! My name is {{agent}}." 43 | val variables: Map = mapOf() 44 | 45 | val exception = 46 | assertThrows { 47 | subject.render( 48 | template, 49 | variables, 50 | ) 51 | } 52 | assertEquals("Undefined keys in template: customer, agent", exception.message) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/AsyncAiServicesTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import dev.langchain4j.data.message.AiMessage 4 | import dev.langchain4j.data.message.SystemMessage 5 | import dev.langchain4j.data.message.UserMessage 6 | import dev.langchain4j.model.chat.ChatModel 7 | import dev.langchain4j.model.chat.request.ChatRequest 8 | import dev.langchain4j.model.chat.response.ChatResponse 9 | import io.kotest.matchers.collections.shouldHaveSize 10 | import io.kotest.matchers.shouldBe 11 | import kotlinx.coroutines.test.runTest 12 | import org.junit.jupiter.api.Test 13 | 14 | internal class AsyncAiServicesTest { 15 | @Test 16 | fun `Should call suspend service`() = 17 | runTest { 18 | val chatResponse = 19 | ChatResponse 20 | .builder() 21 | .aiMessage(AiMessage("Here is your joke: Hello world!")) 22 | .build() 23 | 24 | val model = 25 | object : ChatModel { 26 | override fun chat(chatRequest: ChatRequest): ChatResponse { 27 | chatRequest.messages() shouldHaveSize 2 28 | val systemMessage = 29 | chatRequest.messages().first { it is SystemMessage } as SystemMessage 30 | val userMessage = 31 | chatRequest.messages().first { 32 | it is UserMessage 33 | } as UserMessage 34 | 35 | systemMessage.text() shouldBe "You are a helpful comedian" 36 | userMessage.singleText() shouldBe "Tell me a joke" 37 | 38 | return chatResponse 39 | } 40 | } 41 | 42 | val assistant = 43 | createAiService( 44 | serviceClass = AsyncAssistant::class.java, 45 | factory = AsyncAiServicesFactory(), 46 | ).chatModel(model) 47 | .build() 48 | 49 | val response = assistant.askQuestion() 50 | response shouldBe "Here is your joke: Hello world!" 51 | } 52 | 53 | @Suppress("unused") 54 | interface AsyncAssistant { 55 | @dev.langchain4j.service.SystemMessage("You are a helpful comedian") 56 | @dev.langchain4j.service.UserMessage("Tell me a joke") 57 | suspend fun askQuestion(): String 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/ServiceWithPromptTemplatesTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import dev.langchain4j.data.message.AiMessage 6 | import dev.langchain4j.data.message.SystemMessage 7 | import dev.langchain4j.data.message.UserMessage 8 | import dev.langchain4j.model.chat.ChatModel 9 | import dev.langchain4j.model.chat.request.ChatRequest 10 | import dev.langchain4j.model.chat.response.ChatResponse 11 | import dev.langchain4j.service.AiServices 12 | import dev.langchain4j.service.UserName 13 | import dev.langchain4j.service.V 14 | import io.kotest.matchers.collections.shouldHaveSize 15 | import io.kotest.matchers.shouldBe 16 | import kotlinx.coroutines.test.runTest 17 | import org.junit.jupiter.api.Test 18 | 19 | internal class ServiceWithPromptTemplatesTest { 20 | @Test 21 | fun `Should use System and User Prompt Templates`() = runTest { 22 | val chatResponse = 23 | ChatResponse 24 | .builder() 25 | .aiMessage(AiMessage("I'm fine, thanks")) 26 | .build() 27 | 28 | val model = 29 | object : ChatModel { 30 | override fun chat(chatRequest: ChatRequest): ChatResponse { 31 | chatRequest.messages() shouldHaveSize 2 32 | val systemMessage = 33 | chatRequest.messages().first { it is SystemMessage } as SystemMessage 34 | val userMessage = 35 | chatRequest.messages().first { 36 | it is UserMessage 37 | } as UserMessage 38 | 39 | systemMessage.text() shouldBe 40 | "You are helpful assistant using chatMemoryID=default" 41 | userMessage.singleText() shouldBe "Hello, My friend! How are you?" 42 | 43 | return chatResponse 44 | } 45 | } 46 | 47 | val assistant = 48 | AiServices 49 | .builder(Assistant::class.java) 50 | .systemMessageProvider( 51 | TemplateSystemMessageProvider( 52 | "prompts/ServiceWithTemplatesTest/default-system-prompt.mustache", 53 | ), 54 | ).chatModel(model) 55 | .build() 56 | 57 | val response = assistant.askQuestion(userName = "My friend", question = "How are you?") 58 | assertThat(response).isEqualTo("I'm fine, thanks") 59 | } 60 | 61 | @Suppress("unused") 62 | private interface Assistant { 63 | @dev.langchain4j.service.UserMessage( 64 | "prompts/ServiceWithTemplatesTest/default-user-prompt.mustache", 65 | ) 66 | suspend fun askQuestion( 67 | @UserName userName: String, 68 | @V("message") question: String, 69 | ): String 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/ServiceWithSystemMessageProviderTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import dev.langchain4j.data.message.AiMessage 6 | import dev.langchain4j.data.message.SystemMessage 7 | import dev.langchain4j.model.chat.ChatModel 8 | import dev.langchain4j.model.chat.request.ChatRequest 9 | import dev.langchain4j.model.chat.response.ChatResponse 10 | import dev.langchain4j.service.AiServices 11 | import dev.langchain4j.service.UserMessage 12 | import io.kotest.matchers.collections.shouldHaveSize 13 | import io.kotest.matchers.shouldBe 14 | import org.junit.jupiter.api.Test 15 | 16 | internal class ServiceWithSystemMessageProviderTest { 17 | @Test 18 | fun `Should use SystemMessageProvider`() { 19 | val chatResponse = ChatResponse.builder().aiMessage(AiMessage("I'm fine, thanks")).build() 20 | 21 | val model = 22 | object : ChatModel { 23 | override fun chat(chatRequest: ChatRequest): ChatResponse { 24 | chatRequest.messages() shouldHaveSize 2 25 | val systemMessage = 26 | chatRequest.messages().first { 27 | it is SystemMessage 28 | } as SystemMessage 29 | val userMessage = 30 | chatRequest.messages().first { 31 | it is dev.langchain4j.data.message.UserMessage 32 | } as dev.langchain4j.data.message.UserMessage 33 | systemMessage.text() shouldBe "You are helpful assistant" 34 | userMessage.singleText() shouldBe "How are you" 35 | return chatResponse 36 | } 37 | } 38 | 39 | val assistant = 40 | AiServices 41 | .builder(Assistant::class.java) 42 | .systemMessageProvider( 43 | object : SystemMessageProvider { 44 | override fun getSystemMessage(chatMemoryID: Any): String = 45 | "You are helpful assistant" 46 | }, 47 | ).chatModel(model) 48 | .build() 49 | 50 | val response = assistant.askQuestion("How are you") 51 | assertThat(response).isEqualTo("I'm fine, thanks") 52 | } 53 | 54 | private interface Assistant { 55 | fun askQuestion( 56 | @UserMessage question: String, 57 | ): String 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/TemplateSystemMessageProviderTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import me.kpavlov.langchain4j.kotlin.ChatMemoryId 6 | import me.kpavlov.langchain4j.kotlin.prompt.PromptTemplate 7 | import me.kpavlov.langchain4j.kotlin.prompt.PromptTemplateSource 8 | import me.kpavlov.langchain4j.kotlin.prompt.TemplateRenderer 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.assertThrows 12 | import org.junit.jupiter.api.extension.ExtendWith 13 | import org.mockito.Mock 14 | import org.mockito.junit.jupiter.MockitoExtension 15 | import org.mockito.kotlin.any 16 | import org.mockito.kotlin.eq 17 | import org.mockito.kotlin.whenever 18 | 19 | @ExtendWith(MockitoExtension::class) 20 | internal class TemplateSystemMessageProviderTest { 21 | @Mock 22 | lateinit var templateSourceMock: PromptTemplateSource 23 | 24 | @Mock 25 | lateinit var templateRenderer: TemplateRenderer 26 | 27 | @Mock 28 | lateinit var promptTemplate: PromptTemplate 29 | 30 | @Mock 31 | lateinit var chatMemoryId: ChatMemoryId 32 | 33 | lateinit var templateName: String 34 | 35 | lateinit var subject: TemplateSystemMessageProvider 36 | 37 | @BeforeEach 38 | fun beforeEach() { 39 | templateName = "templateName-${System.currentTimeMillis()}" 40 | subject = TemplateSystemMessageProvider(templateName, templateSourceMock, templateRenderer) 41 | } 42 | 43 | @Test 44 | fun `getSystemMessage returns expected message when template exists`() { 45 | whenever(templateSourceMock.getTemplate(templateName)).thenReturn(promptTemplate) 46 | whenever(promptTemplate.content()).thenReturn("content") 47 | whenever(templateRenderer.render(eq("content"), any())).thenReturn("result") 48 | 49 | val result = subject.getSystemMessage(chatMemoryId) 50 | 51 | assertThat(result).isEqualTo("result") 52 | } 53 | 54 | @Test 55 | fun `getSystemMessage throws IllegalArgumentException when template is not found`() { 56 | whenever(templateSourceMock.getTemplate(templateName)).thenReturn(null) 57 | 58 | assertThrows { 59 | subject.getSystemMessage( 60 | ChatMemoryId(), 61 | ) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/invoker/HybridVirtualThreadInvocationHandlerTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.invoker 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import assertk.assertions.isTrue 6 | import kotlinx.coroutines.test.StandardTestDispatcher 7 | import kotlinx.coroutines.test.TestScope 8 | import org.jetbrains.annotations.Blocking 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.extension.ExtendWith 12 | import org.mockito.Mock 13 | import org.mockito.Mockito.mock 14 | import org.mockito.junit.jupiter.MockitoExtension 15 | import org.mockito.kotlin.verify 16 | import org.mockito.kotlin.whenever 17 | import java.lang.reflect.Method 18 | import java.util.concurrent.CompletableFuture 19 | import java.util.concurrent.CompletionStage 20 | import kotlin.coroutines.Continuation 21 | 22 | @ExtendWith(MockitoExtension::class) 23 | internal class HybridVirtualThreadInvocationHandlerTest { 24 | private lateinit var mockScope: TestScope 25 | 26 | @Mock 27 | private lateinit var mockExecuteSuspend: suspend (Method, Array?) -> Any? 28 | 29 | @Mock 30 | private lateinit var mockExecuteSync: (Method, Array?) -> Any? 31 | private lateinit var handler: HybridVirtualThreadInvocationHandler 32 | 33 | @BeforeEach 34 | fun setUp() { 35 | mockScope = TestScope(StandardTestDispatcher()) 36 | handler = 37 | HybridVirtualThreadInvocationHandler( 38 | scope = mockScope, 39 | executeSuspend = mockExecuteSuspend, 40 | executeSync = mockExecuteSync, 41 | ) 42 | } 43 | 44 | @Test 45 | fun `should handle suspend function invocation`() { 46 | // Given 47 | val proxy = mock() 48 | val method = mock() 49 | val continuation = mock>() 50 | val args = arrayOf("arg1", continuation) 51 | 52 | // Configure a method to appear as a suspend function 53 | whenever(method.parameterTypes).thenReturn( 54 | arrayOf( 55 | String::class.java, 56 | Continuation::class.java, 57 | ), 58 | ) 59 | 60 | // When 61 | val result = handler.invoke(proxy, method, args) 62 | 63 | // Then 64 | assertThat(result).isEqualTo(kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED) 65 | } 66 | 67 | @Test 68 | fun `should handle CompletionStage return type`() { 69 | // Given 70 | val proxy = mock() 71 | val method = mock() 72 | val args = arrayOf("arg1") 73 | 74 | // Configure a method to return CompletionStage 75 | whenever(method.returnType).thenReturn(CompletionStage::class.java) 76 | whenever(method.isAnnotationPresent(Blocking::class.java)).thenReturn(false) 77 | whenever(method.parameterTypes).thenReturn(arrayOf(String::class.java)) 78 | 79 | // When 80 | val result = handler.invoke(proxy, method, args) 81 | 82 | // Then 83 | assertThat(result is CompletableFuture<*>).isTrue() 84 | } 85 | 86 | @Test 87 | fun `should handle synchronous blocking method`() { 88 | // Given 89 | val proxy = mock() 90 | val method = mock() 91 | val args = arrayOf("arg1") 92 | val expectedResult = "result" 93 | 94 | // Configure method as blocking 95 | whenever(method.returnType).thenReturn(String::class.java) 96 | whenever(method.isAnnotationPresent(Blocking::class.java)).thenReturn(true) 97 | whenever(method.parameterTypes).thenReturn(arrayOf(String::class.java)) 98 | 99 | // Configure mock to return an expected result 100 | whenever(mockExecuteSync.invoke(method, args)).thenReturn(expectedResult) 101 | 102 | // When 103 | val result = handler.invoke(proxy, method, args) 104 | 105 | // Then 106 | assertThat(result).isEqualTo(expectedResult) 107 | verify(mockExecuteSync).invoke(method, args) 108 | } 109 | 110 | @Test 111 | fun `should handle synchronous non-blocking method`() { 112 | // Given 113 | val proxy = mock() 114 | val method = mock() 115 | val args = arrayOf("arg1") 116 | val expectedResult = "result" 117 | 118 | // Configure method as non-blocking 119 | whenever(method.returnType).thenReturn(String::class.java) 120 | whenever(method.isAnnotationPresent(Blocking::class.java)).thenReturn(false) 121 | whenever(method.parameterTypes).thenReturn(arrayOf(String::class.java)) 122 | 123 | // Configure mock to return expected result 124 | whenever(mockExecuteSync.invoke(method, args)).thenReturn(expectedResult) 125 | 126 | // When 127 | val result = handler.invoke(proxy, method, args) 128 | 129 | // Then 130 | assertThat(result).isEqualTo(expectedResult) 131 | verify(mockExecuteSync).invoke(method, args) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/invoker/KServicesTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.service.invoker 2 | 3 | import dev.langchain4j.internal.VirtualThreadUtils 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.matchers.shouldBe 6 | import kotlinx.coroutines.test.runTest 7 | import org.junit.jupiter.api.Test 8 | import org.junit.jupiter.api.condition.EnabledForJreRange 9 | import org.junit.jupiter.api.condition.JRE 10 | import java.util.concurrent.CompletableFuture 11 | import java.util.concurrent.CompletionStage 12 | import java.util.concurrent.Executors 13 | 14 | internal class KServicesTest { 15 | internal interface TestService { 16 | suspend fun suspendMethod(param: String): String 17 | 18 | fun syncMethod(param: String): String 19 | 20 | fun completionStageMethod(param: String): CompletionStage 21 | 22 | fun completableFutureMethod(param: String): CompletableFuture 23 | } 24 | 25 | @Test 26 | fun `should handle suspend methods`(): Unit = 27 | runTest { 28 | // Given 29 | val service = KServices.create(TestService::class) 30 | 31 | // When 32 | val result = service.suspendMethod("test") 33 | 34 | // Then 35 | result shouldBe "{param=test}" 36 | } 37 | 38 | @Test 39 | @EnabledForJreRange(min = JRE.JAVA_19, disabledReason = "Requires Java 19 or higher") 40 | fun `should handle sync methods in virtual thread`() { 41 | // Given 42 | val service = KServices.create(TestService::class) 43 | 44 | // When - Run in a virtual thread 45 | val executor = 46 | VirtualThreadUtils.createVirtualThreadExecutor { 47 | Executors.newSingleThreadExecutor() 48 | }!! 49 | val result = 50 | executor 51 | .submit { 52 | service.syncMethod("test") 53 | }.get() 54 | 55 | // Then 56 | result shouldBe "{param=test}" 57 | } 58 | 59 | @Test 60 | fun `should fail sync methods in non-virtual thread`() { 61 | // Given 62 | val service = KServices.create(TestService::class) 63 | 64 | // When/Then - Should throw IllegalStateException when not in a virtual thread 65 | shouldThrow { 66 | service.syncMethod("test") 67 | } 68 | } 69 | 70 | @Test 71 | fun `should handle completion stage methods`() { 72 | // Given 73 | val service = KServices.create(TestService::class) 74 | 75 | // When 76 | val future = 77 | service 78 | .completionStageMethod("test") 79 | .toCompletableFuture() 80 | 81 | // Then 82 | future.join() shouldBe "{param=test}" 83 | } 84 | 85 | @Test 86 | fun `should handle CompletableFuture methods`() { 87 | // Given 88 | val service = KServices.create(TestService::class) 89 | 90 | // When 91 | val future = service.completableFutureMethod("test") 92 | 93 | // Then 94 | future.join() shouldBe "{param=test}" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/resources/data/notes/blumblefang.txt: -------------------------------------------------------------------------------- 1 | The Blumblefang is a big, furry monster with soft purple fur that’s as fuzzy as a blanket. It has big, round eyes that glow yellow in the dark, and it can see through thick forests even at night. Its ears are shaped like bat wings, so it can hear even the tiniest whispers in the wind. The Blumblefang’s nose is large and covered in spots, perfect for sniffing out the juiciest berries and hidden treasures. 2 | 3 | On its back, it has shiny, silver spikes that sparkle like stars, but don’t worry—it's a friendly creature! Its long, curly tail always sways happily behind it, and when it laughs, its big teeth show, but it never bites! Instead, the Blumblefang loves to help lost animals find their way home and tell silly jokes to make everyone giggle. It’s the perfect friend for a wild adventure! -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/resources/data/notes/quantum-computing.txt: -------------------------------------------------------------------------------- 1 | Quantum computing is a type of computing that uses tiny particles called "qubits," which are different from the bits used in regular computers. Regular computers use bits that can either be a 0 or a 1, like light switches being on or off. Quantum computers, however, use qubits, which can be both 0 and 1 at the same time! This special property is called "superposition." It means quantum computers can handle more information at once, making them faster at solving certain kinds of problems. 2 | 3 | Another important thing in quantum computing is "entanglement." When qubits get entangled, the state of one qubit is connected to the state of another, even if they are far apart. This allows quantum computers to process information in a way that regular computers can't. These features make quantum computers potentially much more powerful, but they are still in the early stages, and scientists are working hard to figure out how to use them for real-world problems. -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/resources/prompts/ServiceWithTemplatesTest/default-system-prompt.mustache: -------------------------------------------------------------------------------- 1 | You are helpful assistant using chatMemoryID={{chatMemoryID}} -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/resources/prompts/ServiceWithTemplatesTest/default-user-prompt.mustache: -------------------------------------------------------------------------------- 1 | Hello, {{userName}}! {{message}} -------------------------------------------------------------------------------- /langchain4j-kotlin/src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | # Pattern for logging 3 | org.slf4j.simpleLogger.showDateTime=false 4 | org.slf4j.simpleLogger.showThreadName=true 5 | org.slf4j.simpleLogger.showShortLogName=true 6 | org.slf4j.simpleLogger.levelInBrackets=true 7 | # Set the default logging level for all loggers to TRACE 8 | org.slf4j.simpleLogger.defaultLogLevel=info 9 | # Override the logging level for the specific package 10 | org.slf4j.simpleLogger.log.me.kpavlov.langchain4j.kotlin=trace 11 | org.slf4j.simpleLogger.log.dev.langchain4j=debug 12 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | // https://www.augmentedmind.de/2023/07/30/renovate-bot-cheat-sheet/ 3 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 4 | "extends": [ 5 | "config:recommended", 6 | ":rebaseStalePrs" 7 | ], 8 | "packageRules": [ 9 | { 10 | "matchPackageNames": [ 11 | "me.kpavlov.langchain4j.kotlin" 12 | ], 13 | "enabled": false 14 | }, 15 | { 16 | "description": "Automatically merge minor and patch-level updates", 17 | "matchUpdateTypes": [ 18 | "patch", 19 | "digest" 20 | ], 21 | "automerge": true, 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /reports/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | me.kpavlov.langchain4j.kotlin 6 | root 7 | 0.2.1-SNAPSHOT 8 | ../pom.xml 9 | 10 | 11 | reports 12 | pom 13 | LangChain4j-Kotlin :: Reports 14 | 15 | 16 | true 17 | 18 | 19 | 20 | 21 | me.kpavlov.langchain4j.kotlin 22 | langchain4j-kotlin 23 | ${project.parent.version} 24 | 25 | 26 | 27 | 28 | 29 | 30 | org.jetbrains.kotlinx 31 | kover-maven-plugin 32 | 33 | 34 | true 35 | CLASS 36 | true 37 | 38 | 39 | CLASS 40 | 41 | 42 | COVERED_COUNT 43 | 100 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | kover-xml 52 | 53 | report-xml 54 | 55 | 56 | 57 | kover-verify 58 | 59 | verify 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /samples/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | me.kpavlov.langchain4j.kotlin 6 | 7 | samples 8 | 0.1-SNAPSHOT 9 | LangChain4j-Kotlin :: Samples 10 | This module contains code samples how to use Langchain4j-Kotlin library. 11 | 12 | 13 | UTF-8 14 | official 15 | 17 16 | 1.9.25 17 | ${java.version} 18 | true 19 | ${java.version} 20 | 21 | 0.1.1 22 | 1.9.0 23 | 0.2.0 24 | 1.0.1 25 | 2.0.17 26 | 27 | 28 | 29 | 30 | 31 | org.jetbrains.kotlin 32 | kotlin-bom 33 | ${kotlin.version} 34 | pom 35 | import 36 | 37 | 38 | org.slf4j 39 | slf4j-bom 40 | ${slf4j.version} 41 | pom 42 | import 43 | 44 | 45 | dev.langchain4j 46 | langchain4j-bom 47 | ${langchain4j.version} 48 | pom 49 | import 50 | 51 | 52 | org.jetbrains.kotlinx 53 | kotlinx-coroutines-core-jvm 54 | ${kotlinx-coroutines.version} 55 | 56 | 57 | org.jetbrains.kotlinx 58 | kotlinx-coroutines-test-jvm 59 | ${kotlinx-coroutines.version} 60 | 61 | 62 | 63 | 64 | 65 | 66 | me.kpavlov.langchain4j.kotlin 67 | langchain4j-kotlin 68 | ${langchain4j-kotlin.version} 69 | 70 | 71 | org.jetbrains.kotlinx 72 | kotlinx-coroutines-core-jvm 73 | 74 | 75 | dev.langchain4j 76 | langchain4j-core 77 | 1.0.1 78 | tests 79 | 80 | 81 | dev.langchain4j 82 | langchain4j-open-ai 83 | 84 | 85 | dev.langchain4j 86 | langchain4j 87 | 88 | 89 | org.slf4j 90 | slf4j-simple 91 | runtime 92 | 93 | 94 | me.kpavlov.finchly 95 | finchly 96 | ${finchly.version} 97 | compile 98 | 99 | 100 | org.jetbrains.kotlin 101 | kotlin-test-junit5 102 | ${kotlin.version} 103 | test 104 | 105 | 106 | org.jetbrains.kotlinx 107 | kotlinx-coroutines-test-jvm 108 | test 109 | 110 | 111 | io.kotest 112 | kotest-assertions-core-jvm 113 | 5.9.1 114 | test 115 | 116 | 117 | 118 | 119 | 120 | 121 | org.jetbrains.kotlin 122 | kotlin-maven-plugin 123 | ${kotlin.version} 124 | true 126 | 127 | 128 | compile 129 | 130 | compile 132 | 133 | 134 | ${maven.compiler.release} 135 | ${maven.compiler.parameters} 136 | 137 | ${project.basedir}/src/main/kotlin 138 | ${project.basedir}/src/main/java 139 | 140 | 141 | 142 | 143 | test-compile 144 | 145 | test-compile 147 | 148 | 149 | 150 | ${project.basedir}/src/test/kotlin 151 | ${project.basedir}/src/test/java 152 | 153 | 154 | 155 | 156 | 157 | 158 | org.apache.maven.plugins 159 | maven-compiler-plugin 160 | 3.14.0 161 | 162 | 163 | 164 | default-compile 165 | none 166 | 167 | 168 | 169 | default-testCompile 170 | none 171 | 172 | 173 | java-compile 174 | compile 175 | 176 | compile 177 | 178 | 179 | 180 | java-test-compile 181 | test-compile 182 | 183 | testCompile 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /samples/src/main/java/me/kpavlov/langchain4j/kotlin/samples/ServiceWithTemplateSourceJavaExample.java: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.samples; 2 | 3 | import dev.langchain4j.model.chat.ChatModel; 4 | import dev.langchain4j.model.chat.mock.ChatModelMock; 5 | import dev.langchain4j.service.AiServices; 6 | import dev.langchain4j.service.UserMessage; 7 | import dev.langchain4j.service.UserName; 8 | import dev.langchain4j.service.V; 9 | import me.kpavlov.langchain4j.kotlin.service.TemplateSystemMessageProvider; 10 | import org.slf4j.Logger; 11 | 12 | import static org.slf4j.LoggerFactory.getLogger; 13 | 14 | public class ServiceWithTemplateSourceJavaExample { 15 | 16 | // Use for demo purposes 17 | private static final ChatModel model = new ChatModelMock("Hello"); 18 | private static final Logger LOGGER = getLogger(ServiceWithTemplateSourceJavaExample.class); 19 | 20 | private static final String PROMPT_TEMPLATE_PATH = "prompts/ServiceWithTemplateSourceJavaExample"; 21 | 22 | private interface Assistant { 23 | @UserMessage(PROMPT_TEMPLATE_PATH + "/default-user-prompt.mustache") 24 | String askQuestion( 25 | @UserName String userName, 26 | @V("message") String question 27 | ); 28 | } 29 | 30 | public static void main(String[] args) { 31 | 32 | final var systemMessageProvider = new TemplateSystemMessageProvider( 33 | PROMPT_TEMPLATE_PATH + "/default-system-prompt.mustache" 34 | ); 35 | 36 | final Assistant assistant = AiServices.builder(Assistant.class) 37 | .systemMessageProvider(systemMessageProvider) 38 | .chatModel(model) 39 | .build(); 40 | 41 | String response = assistant.askQuestion("My friend", "How are you?"); 42 | LOGGER.info("AI Response: {}", response); 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/AsyncAiServiceExample.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.samples 2 | 3 | import dev.langchain4j.model.chat.ChatModel 4 | import dev.langchain4j.service.UserMessage 5 | import kotlinx.coroutines.runBlocking 6 | import me.kpavlov.langchain4j.kotlin.service.AsyncAiServicesFactory 7 | import me.kpavlov.langchain4j.kotlin.service.createAiService 8 | 9 | 10 | fun interface AsyncAssistant { 11 | suspend fun askQuestion(@UserMessage question: String): String 12 | } 13 | 14 | class AsyncAiServiceExample( 15 | private val model: ChatModel 16 | ) { 17 | suspend fun callAiService(): String { 18 | val assistant = createAiService( 19 | serviceClass = AsyncAssistant::class.java, 20 | factory = AsyncAiServicesFactory(), 21 | ).chatModel(model) 22 | .systemMessageProvider { "You are a helpful software engineer" } 23 | .build() 24 | 25 | return assistant.askQuestion( 26 | "What's new in Kotlin/AI space in one sentence?" 27 | ) 28 | } 29 | } 30 | 31 | fun main() { 32 | runBlocking { 33 | val response = AsyncAiServiceExample(model).callAiService() 34 | println("AI Answer: \"$response\"") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/ChatModelExample.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.samples 2 | 3 | import dev.langchain4j.data.message.SystemMessage.systemMessage 4 | import dev.langchain4j.data.message.UserMessage.userMessage 5 | import dev.langchain4j.model.chat.mock.ChatModelMock 6 | import kotlinx.coroutines.runBlocking 7 | import me.kpavlov.langchain4j.kotlin.model.chat.chatAsync 8 | 9 | @Suppress("MagicNumber") 10 | fun main() = 11 | runBlocking { 12 | val response = 13 | ChatModelMock("Hello").chatAsync { 14 | messages += systemMessage("You are a helpful assistant") 15 | messages += userMessage("Say Hello") 16 | parameters { 17 | temperature = 0.1 18 | modelName = "gpt-4o-mini" 19 | } 20 | } 21 | println("AI Answer: \"${response.aiMessage().text()}\"") 22 | } 23 | -------------------------------------------------------------------------------- /samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/Environment.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.samples 2 | 3 | import dev.langchain4j.model.chat.ChatModel 4 | import dev.langchain4j.model.openai.OpenAiChatModel 5 | import me.kpavlov.finchly.BaseTestEnvironment 6 | 7 | val testEnv = BaseTestEnvironment() 8 | 9 | val model: ChatModel = OpenAiChatModel.builder() 10 | .modelName("gpt-4o-nano") 11 | .apiKey(testEnv["OPENAI_API_KEY"]) 12 | .build() 13 | -------------------------------------------------------------------------------- /samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/OpenAiChatModelExample.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.samples 2 | 3 | import dev.langchain4j.data.message.SystemMessage.systemMessage 4 | import dev.langchain4j.data.message.UserMessage.userMessage 5 | import dev.langchain4j.model.chat.ChatModel 6 | import dev.langchain4j.model.openai.OpenAiChatRequestParameters 7 | import kotlinx.coroutines.runBlocking 8 | import me.kpavlov.langchain4j.kotlin.model.chat.chatAsync 9 | 10 | class OpenAiChatModelExample( 11 | private val model: ChatModel 12 | ) { 13 | suspend fun callChatAsync(): String { 14 | val response = 15 | model.chatAsync { 16 | messages += systemMessage("You are a helpful assistant") 17 | messages += userMessage("Say Hello") 18 | parameters(OpenAiChatRequestParameters.builder()) { 19 | temperature = 0.1 20 | builder.seed(42) // OpenAI specific parameter 21 | } 22 | } 23 | val result = response.aiMessage().text() 24 | println("AI Answer: \"$result\"") 25 | return result 26 | } 27 | } 28 | 29 | fun main() { 30 | runBlocking { 31 | OpenAiChatModelExample(model).callChatAsync() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/src/main/resources/prompts/ServiceWithTemplateSourceJavaExample/default-system-prompt.mustache: -------------------------------------------------------------------------------- 1 | You are helpful assistant using chatMemoryID={{chatMemoryID}} -------------------------------------------------------------------------------- /samples/src/main/resources/prompts/ServiceWithTemplateSourceJavaExample/default-user-prompt.mustache: -------------------------------------------------------------------------------- 1 | Hello, {{userName}}! {{message}} -------------------------------------------------------------------------------- /samples/src/main/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | # Pattern for logging 3 | org.slf4j.simpleLogger.showDateTime=false 4 | org.slf4j.simpleLogger.showThreadName=true 5 | org.slf4j.simpleLogger.showShortLogName=true 6 | org.slf4j.simpleLogger.levelInBrackets=true 7 | # Set the default logging level for all loggers to TRACE 8 | org.slf4j.simpleLogger.defaultLogLevel=info 9 | # Override the logging level for the specific package 10 | org.slf4j.simpleLogger.log.me.kpavlov.langchain4j.kotlin=info 11 | org.slf4j.simpleLogger.log.dev.langchain4j=debug 12 | -------------------------------------------------------------------------------- /samples/src/test/java/me/kpavlov/langchain4j/kotlin/samples/OpenAiChatModelTest.kt: -------------------------------------------------------------------------------- 1 | package me.kpavlov.langchain4j.kotlin.samples 2 | 3 | import dev.langchain4j.model.chat.ChatModel 4 | import dev.langchain4j.model.chat.mock.ChatModelMock 5 | import io.kotest.matchers.shouldBe 6 | import kotlinx.coroutines.test.runTest 7 | import kotlin.test.Test 8 | 9 | class OpenAiChatModelTest { 10 | private val model: ChatModel = ChatModelMock("Hello from Mock Model") 11 | 12 | @Test 13 | fun `OpenAiChatModel example should work`() = 14 | runTest { 15 | OpenAiChatModelExample(model).callChatAsync() shouldBe "Hello from Mock Model" 16 | } 17 | 18 | @Test 19 | fun `Async AiServices example should work`() = 20 | runTest { 21 | AsyncAiServiceExample(model).callAiService() shouldBe "Hello from Mock Model" 22 | } 23 | } 24 | --------------------------------------------------------------------------------