Skip to content

Commit

Permalink
Refactor and enhance template system handling (#63)
Browse files Browse the repository at this point in the history
Made `SystemMessageProvider` a functional interface and added a default
implementation for `apply`. Introduced new Mustache templates for system
and user prompts. Updated documentation and examples to simplify prompt
template handling. Configured Maven for Kotlin and Java interop while
adding proper compiler settings. Added Java sample demonstrating
template usage.

---------

Co-authored-by: Konstantin Pavlov <{ID}+{username}@users.noreply.github.com>
  • Loading branch information
kpavlov and Konstantin Pavlov authored Jan 11, 2025
1 parent 4ad0b2b commit fa615c2
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 88 deletions.
130 changes: 47 additions & 83 deletions docs/PromptTemplates.md
Original file line number Diff line number Diff line change
@@ -1,119 +1,88 @@
# Customizing Prompt Templates
# Prompt Templates Guide

This guide demonstrates how to configure and use prompt templates with
the [LangChain4J's AiServices](https://docs.langchain4j.dev/tutorials/ai-services). This setup involves configuring
prompt templates, defining and extending prompt template sources and template rendering.
Learn how to use prompt templates with LangChain4J's AiServices. This guide covers setup, configuration, and
customization.

## Creating and Using Prompt Template
## Create Your First Template

Let's start with built-in mechanism of loading prompt template from classpath. Your prompt templates should be located
in the classpath, e.g.

File: `prompts/default-system-prompt.mustache`
Place your prompt templates in the classpath:

System prompt template (path: `prompts/default-system-prompt.mustache`):
```mustache
You are helpful assistant using chatMemoryID={{chatMemoryID}}
```

File: `prompts/default-user-prompt.mustache`

User prompt template (path: `prompts/default-user-prompt.mustache`):
```mustache
Hello, {{userName}}! {{message}}
```

## Example with AiServices
## Quick Start

Define an interface that uses these templates and configure the AiServices builder:
Here's how to use templates in your code:

```kotlin
// Define assistant interface
// Define your assistant
interface Assistant {
@UserMessage(
// "Hello, {{userName}}! {{message}}"
"prompts/default-user-prompt.mustache", //template name/resource
)
@UserMessage("prompts/default-user-prompt.mustache")
fun askQuestion(
@UserName userName: String,
@UserName userName: String, // Compile with javac `parameters=true`
@V("message") question: String,
): String
}

// Define Chain of Thoughts
// Set up the assistant
val assistant: Assistant =
AiServices
.builder(Assistant::class.java)
.systemMessageProvider(
TemplateSystemMessageProvider(
// "You are helpful assistant using chatMemoryID={{chatMemoryID}}"
"prompts/default-system-prompt.mustache", // template name/recource
),
TemplateSystemMessageProvider("prompts/default-system-prompt.mustache")
).chatLanguageModel(model)
.build()

// Run it!
val response =
assistant.askQuestion(
userName = "My friend",
question = "How are you?",
)
// Use it
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?"

## How does it work
This creates:

In the default implementation, `TemplateSystemMessageProvider` handles the system prompt template and `AiServices` uses
the templates to generate prompts.
- System prompt: "You are helpful assistant using chatMemoryID=default"
- User prompt: "Hello, My friend! How are you?"

`PromptTemplateFactory` provides `PromptTemplateFactory.Template` for `AiServices`. It is registered automatically via
Java ServiceLoaders mechanism. This class is responsible for obtaining prompt templates from a `PromptTemplateSource`.
If the specified template cannot be found, it will fallback to using default LC4J's the input template content.
## Under the Hood

`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.
Key components:

Implementers of the `TemplateRenderer` interface will typically replace placeholders in the template with corresponding
values from the variables map.
- `PromptTemplateFactory`: Gets templates and handles defaults
- `ClasspathPromptTemplateSource`: Loads templates from your classpath
- `SimpleTemplateRenderer`: Replaces `{{key}}` placeholders with values
- `RenderablePromptTemplate`: Connects everything together

`SimpleTemplateRenderer` finds and replaces placeholders in the template in the Mustache-like 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.
## Customize Your Setup

`RenderablePromptTemplate` implements both `PromptTemplate` and LangChain4j's `PromptTemplateFactory.Template`
interfaces. It uses a `TemplateRenderer` to render the template content using provided variables.
Configure templates in `langchain4j-kotlin.properties`:

## Customization
| Setting | Purpose | Default |
|----------------------------|---------------------------|---------------------------------|
| `prompt.template.source` | Where templates load from | `ClasspathPromptTemplateSource` |
| `prompt.template.renderer` | How templates render | `SimpleTemplateRenderer` |

You may customize templates via configuration file `langchain4j-kotlin.properties`, located in the classpath.
### Add Custom Template Sources

| Key | Description | Default Value |
|----------------------------|-------------------|----------------------------------------------------------------------|
| `prompt.template.source` | Template source | `me.kpavlov.langchain4j.kotlin.prompt.ClasspathPromptTemplateSource` |
| `prompt.template.renderer` | Template renderer | `me.kpavlov.langchain4j.kotlin.prompt.SimpleTemplateRenderer` |

### Extending PromptTemplateSource

To create a custom template source, implement the PromptTemplateSource interface:
Create your own source by implementing `PromptTemplateSource`:

```kotlin
interface PromptTemplateSource {
fun getTemplate(name: TemplateName): PromptTemplate?
}
```

Example implementation for Redis and Jedis:
Example using Redis:

```kotlin
package com.example

// Redis/Jedis-backed template source

class RedisPromptTemplateSource(private val jedis: Jedis) : PromptTemplateSource {
override fun getTemplate(name: TemplateName): PromptTemplate? {
return jedis.get(name)?.let {
Expand All @@ -123,15 +92,14 @@ class RedisPromptTemplateSource(private val jedis: Jedis) : PromptTemplateSource
}
```

Register your implementation in the `langchain4j-kotlin.properties` configuration file:

Enable it in your properties:
```properties
prompt.template.source=com.example.RedisPromptTemplateSource
```

### Extending TemplateRenderer
### Create Custom Renderers

To create a custom template renderer, implement the TemplateRenderer interface:
Build your own renderer:

```kotlin
interface TemplateRenderer {
Expand All @@ -142,27 +110,23 @@ interface TemplateRenderer {
}
```

Example implementation:

Example:
```kotlin
package com.example

// Freemarker-based renderer
class MyTemplateRenderer : TemplateRenderer {

override fun render(template: TemplateContent, variables: Map<String, Any?>): String {
TODO("Add implementation here")
}
}
```

Register your implementation in the `langchain4j-kotlin.properties` configuration file:

Enable it:
```properties
prompt.template.renderer=com.example.MyTemplateRenderer
```

## Examples
## Learn More

Find complete examples:

You may find the unit test with the
example [here](../langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/ServiceWithPromptTemplatesTest.kt)
- [Unit test example](../langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/service/ServiceWithPromptTemplatesTest.kt)
- [Using from Java](../samples/src/main/java/me/kpavlov/langchain4j/kotlin/samples/ServiceWithTemplateSourceJavaExample.java)
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,27 @@ package me.kpavlov.langchain4j.kotlin.service

import me.kpavlov.langchain4j.kotlin.ChatMemoryId
import me.kpavlov.langchain4j.kotlin.PromptContent
import java.util.function.Function

/**
* Interface for providing LLM system messages based on a given chat memory identifier.
*/
public interface SystemMessageProvider {
@FunctionalInterface
interface SystemMessageProvider : Function<ChatMemoryId, PromptContent?> {
/**
* Provides a system message based on the given chat memory identifier.
*
* @param chatMemoryID Identifier for the chat memory used to generate the system message.
* @return A system prompt string associated with the provided chat memory identifier, maybe `null`
*/
public fun getSystemMessage(chatMemoryID: ChatMemoryId): PromptContent?
fun getSystemMessage(chatMemoryID: ChatMemoryId): PromptContent?

/**
* Applies the given chat memory identifier to generate the corresponding system message.
*
* @param chatMemoryID The identifier representing the chat memory to be used.
* @return The prompt content associated with the specified chat memory identifier,
* or `null` if no system message is available.
*/
override fun apply(chatMemoryID: ChatMemoryId): PromptContent? = getSystemMessage(chatMemoryID)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ import me.kpavlov.langchain4j.kotlin.prompt.TemplateRenderer
* @property promptTemplateSource Source from which the prompt templates are fetched.
* @property promptTemplateRenderer Renderer used to render the content with specific variables.
*/
public open class TemplateSystemMessageProvider(
open class TemplateSystemMessageProvider(
private val templateName: TemplateName,
private val promptTemplateSource: PromptTemplateSource = Configuration.promptTemplateSource,
private val promptTemplateRenderer: TemplateRenderer = Configuration.promptTemplateRenderer,
) : SystemMessageProvider {
public open fun templateName(): TemplateName = templateName
open fun templateName(): TemplateName = templateName

constructor(
templateName: TemplateName,
) : this(
templateName = templateName,
promptTemplateSource = Configuration.promptTemplateSource,
promptTemplateRenderer = Configuration.promptTemplateRenderer,
)

/**
* Generates a system message using a template and the provided chat memory identifier.
Expand Down
11 changes: 10 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@
<kotlin.code.style>official</kotlin.code.style>
<java.version>17</java.version>
<kotlin.version>2.1.0</kotlin.version>
<kotlin.compiler.apiVersion>1.9</kotlin.compiler.apiVersion>
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
<kotlin.compiler.languageVersion>1.9</kotlin.compiler.languageVersion>
<kotlin.compiler.apiVersion>1.9</kotlin.compiler.apiVersion>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.release>${java.version}</maven.compiler.release>
<maven.compiler.source>${java.version}</maven.compiler.source>
<!-- Dependencies -->
Expand Down Expand Up @@ -277,6 +278,14 @@
<multiPlatform>true</multiPlatform>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<parameters>true</parameters>
</configuration>
</plugin>
</plugins>
</pluginManagement>

Expand Down
64 changes: 64 additions & 0 deletions samples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
Expand All @@ -52,4 +56,64 @@

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<extensions>true</extensions>
<executions>
<execution>
<id>compile</id>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/main/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<configuration>
<sourceDirs>
<sourceDir>${project.basedir}/src/test/kotlin</sourceDir>
<sourceDir>${project.basedir}/src/test/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<!-- Replacing default-compile as it is treated specially by Maven -->
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<!-- Replacing default-testCompile as it is treated specially by Maven -->
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<goals>
<goal>compile</goal>
</goals>
<phase>compile</phase>
</execution>
<execution>
<id>java-test-compile</id>
<goals>
<goal>testCompile</goal>
</goals>
<phase>test-compile</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
Loading

0 comments on commit fa615c2

Please sign in to comment.