Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@ ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999
# noinspection EditorConfigKeyCorrectness
ktlint_function_naming_ignore_when_annotated_with = "Test"
ij_kotlin_packages_to_use_import_on_demand = unset

[*.mustache]
insert_final_newline = false
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ Kotlin enhancements for [LangChain4j](https://github.com/langchain4j/langchain4j

See the [discussion](https://github.com/langchain4j/langchain4j/discussions/1897) on LangChain4j project.

<p style="background-color: powderblue; padding: 10px; border-radius: 10px">
ℹ️ I am verifying my ideas for improving LangChain4j here.
If an idea is accepted, the code might be adopted into the original [LangChain4j](https://github.com/langchain4j) project. If not - you may enjoy it here.
</p>

## Features

- ✨ [Kotlin Coroutine](https://kotlinlang.org/docs/coroutines-guide.html) support for [ChatLanguageModels](https://docs.langchain4j.dev/tutorials/chat-and-language-models)
- 🌊 [Kotlin Asynchronous Flow](https://kotlinlang.org/docs/flow.html) support for [StreamingChatLanguageModels](https://docs.langchain4j.dev/tutorials/ai-services#streaming)
- 💄[External Prompt Templates](docs/PromptTemplates.md) support. Basic implementation loads both system and user prompt
templates from the classpath,
but [PromptTemplateSource](langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/prompt/PromptTemplateSource.kt)
provides extension mechanism.

## Installation

Expand Down
90 changes: 90 additions & 0 deletions docs/PromptTemplates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Using Prompt Templates with AiServices

This guide demonstrates how to configure and use prompt templates with the AiServices module. This setup involves
configuring prompt templates, creating prompt template sources, and rendering templates with specific variables.
Additionally, a test class validates the functionality of these templates.

## Configuration

First, make sure you have the necessary configuration in your `langchain4j-kotlin.properties` file. This is default
configuration:

```properties
prompt.template.source=me.kpavlov.langchain4j.kotlin.prompt.ClasspathPromptTemplateSource
prompt.template.renderer=me.kpavlov.langchain4j.kotlin.prompt.SimpleTemplateRenderer
```

This configuration specifies the source and renderer for the prompt templates.

## Creating and Using Prompt Template

Include the necessary classes. Your prompt templates will be sourced from the classpath and rendered using a simple
template renderer.

File: default-system-prompt.mustache

```mustache
You are helpful assistant using chatMemoryID={{chatMemoryID}}
```

File: default-user-prompt.mustache

```mustache
Hello, {{userName}}! {{message}}
```

## Example with AiServices

Define an interface that uses these templates and configure the AiServices builder:

```kotlin
private interface Assistant {
@UserMessage("default-user-prompt.mustache")
fun askQuestion(@UserName userName: String, @V("message") question: String): String
}
```

Create and run your assistant:

```kotlin
val assistant = AiServices.builder(Assistant::class.java)
.systemMessageProvider(TemplateSystemMessageProvider("default-system-prompt.mustache"))
.chatLanguageModel(model)
.build()

val response = assistant.askQuestion(
userName = "My friend",
question = "How are you?"
)
```

System and user prompts will be:

- **System prompt:** "You are helpful assistant using chatMemoryID=default"
- **User Prompt:** "Hello, My friend! How are you?"

In this example, `TemplateSystemMessageProvider` handles the system prompt template and `AiServices` uses the templates
to generate responses.

`PromptTemplateFactory` is registered automatically via Java ServiceLoaders mechanism and is used to provide
`PromptTemplate`. This class is responsible for obtaining prompt templates from a `PromptTemplateSource` and rendering
them using a `TemplateRenderer`. If the specified template cannot be found, it will fallback to using the input template
content.

`ClasspathPromptTemplateSource` is implementing `PromptTemplateSource` interface and provides a mechanism to load prompt
templates from the classpath using the template name as the resource identifier. It attempts to locate the template file
in the classpath and reads its contents as the template data. It is registered via property file and might be
overridden.

Implementers of the `TemplateRenderer` interface will typically replace placeholders in the template with corresponding
values from the variables map.

`SimpleTemplateRenderer` finds and replaces placeholders in the template in the format `{{key}}`, where `key`
corresponds to an entry in the variables map. If any placeholders in the template are not defined in the variables map,
an `IllegalArgumentException` will be thrown.

`RenderablePromptTemplate` implements both `PromptTemplate` and LangChain4j's `PromptTemplateFactory.Template`
interfaces. It uses a `TemplateRenderer` to render the template content using provided variables.

You may find the unit test with the
example [here](../langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/ServiceWithPromptTemplatesTest.kt)
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package me.kpavlov.langchain4j.kotlin

import me.kpavlov.langchain4j.kotlin.prompt.PromptTemplateSource
import me.kpavlov.langchain4j.kotlin.prompt.TemplateRenderer
import java.util.Properties

/**
* Configuration is an object responsible for loading and providing access to application properties.
*
* This object provides utilities to access various configuration settings and components such as prompt templates and
* their renderers. The configurations are loaded from a properties file, and components are instantiated dynamically
* based on the class names specified in the properties.
*/
object Configuration {
val properties: Properties = loadProperties()

operator fun get(key: String): String = properties.getProperty(key)

val promptTemplateSource: PromptTemplateSource =
createInstanceByName(this["prompt.template.source"])
val promptTemplateRenderer: TemplateRenderer =
createInstanceByName(this["prompt.template.renderer"])
}

private fun loadProperties(fileName: String = "langchain4j-kotlin.properties"): Properties {
val properties = Properties()
val classLoader = Thread.currentThread().contextClassLoader
classLoader.getResourceAsStream(fileName).use { inputStream ->
require(inputStream != null) {
"Property file '$fileName' not found in the classpath"
}
properties.load(inputStream)
}
return properties
}

@Suppress("UNCHECKED_CAST", "TooGenericExceptionCaught")
private fun <T> createInstanceByName(className: String): T =
try {
// Get the class object by name
val clazz = Class.forName(className)
// Create an instance of the class
clazz.getDeclaredConstructor().newInstance() as T
} catch (e: Exception) {
throw IllegalArgumentException("Can't create $className", e)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
package me.kpavlov.langchain4j.kotlin

/**
* Type alias representing an identifier for a chat memory.
*
* This type alias is used within the `SystemMessageProvider` interface
* and its implementations to specify the input parameter for retrieving
* system messages.
*/
typealias ChatMemoryId = Any

/**
* Type alias for the name of a template.
*
* This alias is used to represent template names as strings in various parts
* of the codebase, providing a clearer and more specific meaning compared
* to using `String` directly.
*/
typealias TemplateName = String

/**
* Represents the content of a template.
*
* This type alias is used to define a standard type for template content within the system,
* which is expected to be in the form of a string. Various classes and functions that deal
* with templates will utilize this type alias to ensure consistency and clarity.
*/
typealias TemplateContent = String

/**
* Type alias for a string representing the content of a prompt.
*
* This alias is used to define the type of content that can be returned
* by various functions and methods within the system that deal with
* generating and handling prompts.
*/
typealias PromptContent = String
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ package me.kpavlov.langchain4j.kotlin.internal

import org.slf4j.MarkerFactory

internal val PII = MarkerFactory.getMarker("PII")
internal val SENSITIVE = MarkerFactory.getMarker("SENSITIVE")
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import dev.langchain4j.model.output.Response
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import me.kpavlov.langchain4j.kotlin.internal.PII
import org.slf4j.LoggerFactory

private val logger = LoggerFactory.getLogger(StreamingChatLanguageModel::class.java)
Expand Down Expand Up @@ -78,7 +77,7 @@ fun StreamingChatLanguageModel.generateFlow(
object : StreamingResponseHandler<AiMessage> {
override fun onNext(token: String) {
logger.trace(
me.kpavlov.langchain4j.kotlin.internal.PII,
me.kpavlov.langchain4j.kotlin.internal.SENSITIVE,
"Received token: {}",
token,
)
Expand All @@ -87,7 +86,7 @@ fun StreamingChatLanguageModel.generateFlow(

override fun onComplete(response: Response<AiMessage>) {
logger.trace(
me.kpavlov.langchain4j.kotlin.internal.PII,
me.kpavlov.langchain4j.kotlin.internal.SENSITIVE,
"Received response: {}",
response,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package me.kpavlov.langchain4j.kotlin.prompt

import me.kpavlov.langchain4j.kotlin.TemplateName

/**
* Classpath-based implementation of [PromptTemplateSource].
*
* This class provides a mechanism to load prompt templates from the classpath
* using the template name as the resource identifier. It attempts to locate the
* template file in the classpath and reads its contents as the template data.
*/
public open class ClasspathPromptTemplateSource : PromptTemplateSource {
/**
* Retrieves a prompt template based on the provided template name.
*
* This method attempts to locate the template file in the classpath using the given
* template name as the resource identifier. If found, it reads the contents of the
* file and returns a [SimplePromptTemplate] containing the template data.
*
* @param name The name of the template to retrieve.
* @return The prompt template associated with the specified name, or null if no such template exists.
*/
override fun getTemplate(name: TemplateName): PromptTemplate? {
val resourceStream = this::class.java.classLoader.getResourceAsStream(name)
return resourceStream?.bufferedReader()?.use { reader ->
val content = reader.readText()
return SimplePromptTemplate(content)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package me.kpavlov.langchain4j.kotlin.prompt

import me.kpavlov.langchain4j.kotlin.TemplateContent

/**
* Interface representing a template for prompts.
*
* Implementations of this interface provide the template content that can be used
* to generate prompts. The content is expected to be a string format that can
* incorporate variables or placeholders.
*/
public interface PromptTemplate {
fun content(): TemplateContent
}

/**
* Data class representing a simple implementation of the [PromptTemplate] interface.
*
* This class provides a concrete implementation of the [PromptTemplate] interface by
* storing the template content and returning it via the `content` method. It is
* designed to work with prompt templates loaded from various sources.
*
* @param content The content of the template, represented as [TemplateContent].
*/
public data class SimplePromptTemplate(
private val content: TemplateContent,
) : PromptTemplate {
override fun content(): TemplateContent = content
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package me.kpavlov.langchain4j.kotlin.prompt

import dev.langchain4j.spi.prompt.PromptTemplateFactory
import me.kpavlov.langchain4j.kotlin.Configuration
import me.kpavlov.langchain4j.kotlin.internal.SENSITIVE
import org.slf4j.Logger

/**
* Factory class for creating instances of [RenderablePromptTemplate].
*
* This class is responsible for obtaining prompt templates from a [PromptTemplateSource]
* and rendering them using a [TemplateRenderer]. If the specified template cannot be found,
* it will fallback to using the input template content.
*
* @constructor Creates an instance of [PromptTemplateFactory] using the provided source and renderer.
* @property logger Logger instance for logging information and debugging messages.
* @property source Source to obtain prompt templates by their name.
* @property renderer Renderer used to render the templates with provided variables.
*/
public open class PromptTemplateFactory : PromptTemplateFactory {
protected val logger: Logger = org.slf4j.LoggerFactory.getLogger(javaClass)

private val source: PromptTemplateSource = Configuration.promptTemplateSource
private val renderer: TemplateRenderer = Configuration.promptTemplateRenderer

override fun create(input: PromptTemplateFactory.Input): PromptTemplateFactory.Template {
logger.info(
"Create PromptTemplate input.template = ${input.template}, input.name = ${input.name}",
)
val template = source.getTemplate(input.template)
return if (template == null) {
if (logger.isTraceEnabled) {
logger.trace(
SENSITIVE,
"Prompt template not found, failing back to input.template=\"{}\"",
input.template,
)
} else {
logger.debug(
"Prompt template not found, failing back to input.template",
)
}
RenderablePromptTemplate(
name = input.name,
content = input.template,
templateRenderer = renderer,
)
} else {
if (logger.isTraceEnabled) {
logger.trace(
"Found Prompt template by name=\"{}\": \"{}\"",
input.template,
template,
)
} else {
logger.debug(
"Found Prompt template by name=\"{}\"",
input.template,
)
}
RenderablePromptTemplate(
name = input.template,
content = template.content(),
templateRenderer = renderer,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package me.kpavlov.langchain4j.kotlin.prompt

import me.kpavlov.langchain4j.kotlin.TemplateName

/**
* Interface for obtaining prompt templates by their name.
*
* This interface defines a method for retrieving a prompt template using
* a template name. The implementation of this interface will determine
* how and from where the templates are sourced.
*/
interface PromptTemplateSource {
/**
* Retrieves a prompt template based on the provided template name.
*
* @param name The name of the template to retrieve.
* @return The prompt template associated with the specified name, or null if no such template exists.
*/
fun getTemplate(name: TemplateName): PromptTemplate?
}
Loading
Loading