Skip to content

Commit 4ad0b2b

Browse files
kpavlovKonstantin Pavlov
and
Konstantin Pavlov
authored
Chat request builders (#62)
Introduced `ChatRequestBuilder` and `chatRequest` extensions to simplify constructing and configuring ChatRequests. Added corresponding async methods, tests, and updated examples to enhance usability and ensure consistent behavior. Introduced a new `samples` module to demonstrate the usage of the LangChain4j-Kotlin library. Includes a `ChatModel` example and updates the root `pom.xml` to include the module. Updated dependencies and configurations to support Kotlin and the newly added artifacts. Upgraded `langchain4j` dependencies to `1.0.0-alpha1` and replaced outdated builder syntax with concise Kotlin DSL in code examples. These changes improve clarity, maintain modernity, and ensure compatibility with the latest API. Introduce a new `OpenAiChatModelExample.kt` showcasing asynchronous chat with OpenAI's GPT-4-based model. Updated `pom.xml` to include required dependencies, renamed `ChatModel.kt` to `ChatModelExample.kt`, and added logging configuration via `simplelogger.properties`. Enhanced README with updated sample references. --------- Co-authored-by: Konstantin Pavlov <{ID}+{username}@users.noreply.github.com>
1 parent 57a1a0d commit 4ad0b2b

File tree

11 files changed

+444
-77
lines changed

11 files changed

+444
-77
lines changed

README.md

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ Add the following dependencies to your `pom.xml`:
4747
<dependency>
4848
<groupId>dev.langchain4j</groupId>
4949
<artifactId>langchain4j</artifactId>
50-
<version>0.36.2</version>
50+
<version>1.0.0-alpha1</version>
5151
</dependency>
5252
<dependency>
5353
<groupId>dev.langchain4j</groupId>
5454
<artifactId>langchain4j-open-ai</artifactId>
55-
<version>0.36.2</version>
55+
<version>1.0.0-alpha1</version>
5656
</dependency>
5757
</dependencies>
5858
```
@@ -64,7 +64,7 @@ Add the following to your `build.gradle.kts`:
6464
```kotlin
6565
dependencies {
6666
implementation("me.kpavlov.langchain4j.kotlin:langchain4j-kotlin:$LATEST_VERSION")
67-
implementation("dev.langchain4j:langchain4j-open-ai:0.36.2")
67+
implementation("dev.langchain4j:langchain4j-open-ai:1.0.0-alpha1")
6868
}
6969
```
7070

@@ -82,35 +82,32 @@ val model: ChatLanguageModel = OpenAiChatModel.builder()
8282

8383
// sync call
8484
val response =
85-
model.chat(
86-
ChatRequest
87-
.builder()
88-
.messages(
89-
listOf(
90-
SystemMessage.from("You are a helpful assistant"),
91-
UserMessage.from("Hello!"),
92-
),
93-
).build(),
94-
)
85+
model.chat(chatRequest {
86+
messages += systemMessage("You are a helpful assistant")
87+
messages += userMessage("Hello!")
88+
})
9589
println(response.aiMessage().text())
9690

9791
// Using coroutines
9892
CoroutineScope(Dispatchers.IO).launch {
9993
val response =
100-
model.chatAsync(
101-
ChatRequest
102-
.builder()
103-
.messages(
104-
listOf(
105-
SystemMessage.from("You are a helpful assistant"),
106-
UserMessage.from("Hello!"),
107-
),
108-
),
109-
)
94+
model.chatAsync {
95+
messages += systemMessage("You are a helpful assistant")
96+
messages += userMessage("Say Hello")
97+
parameters(OpenAiChatRequestParameters.builder()) {
98+
temperature(0.1)
99+
seed(42) // OpenAI specific parameter
100+
}
101+
}
110102
println(response.aiMessage().text())
111103
}
112104
```
113105

106+
Sample code:
107+
108+
- [ChatModelExample.kt](samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/ChatModelExample.kt)
109+
- [OpenAiChatModelExample.kt](samples/src/main/kotlin/me/kpavlov/langchain4j/kotlin/samples/OpenAiChatModelExample.kt)
110+
114111
### Streaming Chat Language Model support
115112

116113
Extension can convert [StreamingChatLanguageModel](https://docs.langchain4j.dev/tutorials/response-streaming) response into [Kotlin Asynchronous Flow](https://kotlinlang.org/docs/flow.html):
@@ -157,9 +154,12 @@ You can easyly get started with LangChain4j-Kotlin notebooks:
157154
// ... or add project's target/classes to classpath
158155
//@file:DependsOn("../target/classes")
159156

160-
import dev.langchain4j.data.message.*
157+
import dev.langchain4j.data.message.SystemMessage.systemMessage
158+
import dev.langchain4j.data.message.UserMessage.userMessage
161159
import dev.langchain4j.model.openai.OpenAiChatModel
160+
162161
import me.kpavlov.langchain4j.kotlin.model.chat.generateAsync
162+
163163

164164
val model = OpenAiChatModel.builder()
165165
.apiKey("demo")
@@ -174,8 +174,8 @@ val scope = CoroutineScope(Dispatchers.IO)
174174
runBlocking {
175175
val result = model.generateAsync(
176176
listOf(
177-
SystemMessage.from("You are helpful assistant"),
178-
UserMessage.from("Make a haiku about Kotlin, Langchani4j and LLM"),
177+
systemMessage("You are helpful assistant"),
178+
userMessage("Make a haiku about Kotlin, Langchani4j and LLM"),
179179
)
180180
)
181181
println(result.content().text())

langchain4j-kotlin/src/main/kotlin/me/kpavlov/langchain4j/kotlin/model/chat/ChatLanguageModelExtensions.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import dev.langchain4j.model.chat.request.ChatRequest
77
import dev.langchain4j.model.chat.response.ChatResponse
88
import dev.langchain4j.model.output.Response
99
import kotlinx.coroutines.coroutineScope
10+
import me.kpavlov.langchain4j.kotlin.model.chat.request.ChatRequestBuilder
11+
import me.kpavlov.langchain4j.kotlin.model.chat.request.chatRequest
1012

1113
/**
1214
* Asynchronously processes a chat request using the language model within
@@ -59,6 +61,32 @@ suspend fun ChatLanguageModel.chatAsync(request: ChatRequest): ChatResponse {
5961
suspend fun ChatLanguageModel.chatAsync(requestBuilder: ChatRequest.Builder): ChatResponse =
6062
chatAsync(requestBuilder.build())
6163

64+
/**
65+
* Asynchronously processes a chat request by configuring a `ChatRequest`
66+
* using a provided builder block. This method facilitates the creation
67+
* of well-structured chat requests using a `ChatRequestBuilder` and
68+
* executes the request using the associated `ChatLanguageModel`.
69+
*
70+
* Example usage:
71+
* ```kotlin
72+
* model.chatAsync {
73+
* messages += systemMessage("You are a helpful assistant")
74+
* messages += userMessage("Say Hello")
75+
* parameters {
76+
* temperature(0.1)
77+
* }
78+
* }
79+
* ```
80+
*
81+
* @param block A lambda with receiver on `ChatRequestBuilder` used to
82+
* configure the messages and parameters for the chat request.
83+
* @return A `ChatResponse` containing the response from the model and any
84+
* associated metadata.
85+
* @throws Exception if the chat request fails or encounters an error during execution.
86+
*/
87+
suspend fun ChatLanguageModel.chatAsync(block: ChatRequestBuilder.() -> Unit): ChatResponse =
88+
chatAsync(chatRequest(block))
89+
6290
/**
6391
* Processes a chat request using a [ChatRequest.Builder] for convenient request
6492
* configuration. This extension function provides a builder pattern alternative
@@ -112,3 +140,13 @@ suspend fun ChatLanguageModel.generateAsync(messages: List<ChatMessage>): Respon
112140
val model = this
113141
return coroutineScope { model.generate(messages) }
114142
}
143+
144+
/**
145+
* Asynchronously generates a response from the chat language model based on the provided messages.
146+
*
147+
* @param messages A variable number of chat messages that serve as the input for the language model's generation.
148+
* This typically includes the conversation history and the current prompt.
149+
* @return A [Response] containing the generated [AiMessage] from the language model.
150+
*/
151+
suspend fun ChatLanguageModel.generateAsync(vararg messages: ChatMessage): Response<AiMessage> =
152+
this.generateAsync(messages.toList())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package me.kpavlov.langchain4j.kotlin.model.chat.request
2+
3+
import dev.langchain4j.data.message.ChatMessage
4+
import dev.langchain4j.model.chat.request.ChatRequest
5+
import dev.langchain4j.model.chat.request.ChatRequestParameters
6+
import dev.langchain4j.model.chat.request.DefaultChatRequestParameters
7+
8+
/**
9+
* Builds and returns a `ChatRequest` using the provided configuration block.
10+
* The configuration is applied on a `ChatRequestBuilder` instance to customize
11+
* messages and parameters that will be part of the resulting `ChatRequest`.
12+
*
13+
* @param block A lambda with receiver on `ChatRequestBuilder` to configure messages
14+
* and/or parameters for the `ChatRequest`.
15+
* @return A fully constructed `ChatRequest` instance based on the applied configurations.
16+
*/
17+
fun chatRequest(block: ChatRequestBuilder.() -> Unit): ChatRequest {
18+
val builder = ChatRequestBuilder()
19+
builder.apply { block() }
20+
return builder.build()
21+
}
22+
23+
/**
24+
* Builder class for constructing a `ChatRequest` instance. Allows configuring
25+
* messages and request parameters to customize the resulting request.
26+
*
27+
* This builder provides methods to add individual or multiple chat messages,
28+
* as well as set request parameters for the generated `ChatRequest`.
29+
*/
30+
open class ChatRequestBuilder(
31+
var messages: MutableList<ChatMessage> = mutableListOf(),
32+
var parameters: ChatRequestParameters? = null,
33+
) {
34+
/**
35+
* Adds a list of `ChatMessage` objects to the builder's messages collection.
36+
*
37+
* @param value The list of `ChatMessage` objects to be added to the builder.
38+
* @return This builder instance for chaining other method calls.
39+
*/
40+
fun messages(value: List<ChatMessage>) = apply { this.messages.addAll(value) }
41+
42+
/**
43+
* Adds a chat message to the messages list.
44+
*
45+
* @param value The chat message to be added.
46+
* @return The current instance for method chaining.
47+
*/
48+
fun message(value: ChatMessage) = apply { this.messages.add(value) }
49+
50+
/**
51+
* Builds and returns a ChatRequest instance using the current state of messages and parameters.
52+
*
53+
* @return A new instance of ChatRequest configured with the provided messages and parameters.
54+
*/
55+
internal fun build(): ChatRequest =
56+
ChatRequest
57+
.Builder()
58+
.messages(this.messages)
59+
.parameters(this.parameters)
60+
.build()
61+
62+
/**
63+
* Configures and sets the parameters for the chat request.
64+
*
65+
* @param builder The builder instance used to create the chat request parameters.
66+
* Defaults to an instance of `DefaultChatRequestParameters.Builder`.
67+
* @param block A lambda with the builder as receiver to configure the chat request parameters.
68+
*/
69+
@JvmOverloads
70+
fun <B : DefaultChatRequestParameters.Builder<*>> parameters(
71+
@Suppress("UNCHECKED_CAST")
72+
builder: B = DefaultChatRequestParameters.builder() as B,
73+
block: B.() -> Unit,
74+
) {
75+
this.parameters = builder.apply(block).build()
76+
}
77+
}

langchain4j-kotlin/src/test/kotlin/me/kpavlov/langchain4j/kotlin/ChatLanguageModelIT.kt

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import dev.langchain4j.data.document.Document
77
import dev.langchain4j.data.message.SystemMessage
88
import dev.langchain4j.data.message.UserMessage
99
import dev.langchain4j.model.chat.ChatLanguageModel
10-
import dev.langchain4j.model.chat.request.ChatRequest
1110
import dev.langchain4j.model.chat.request.ResponseFormat
1211
import dev.langchain4j.model.openai.OpenAiChatModel
1312
import kotlinx.coroutines.test.runTest
@@ -33,7 +32,7 @@ internal class ChatLanguageModelIT {
3332
.apiKey(TestEnvironment.openaiApiKey)
3433
.modelName("gpt-4o-mini")
3534
.temperature(0.0)
36-
.maxTokens(1024)
35+
.maxTokens(512)
3736
.build()
3837

3938
private lateinit var document: Document
@@ -49,18 +48,15 @@ internal class ChatLanguageModelIT {
4948
runTest {
5049
val response =
5150
model.generateAsync(
52-
listOf(
53-
SystemMessage.from(
54-
"""
55-
You are helpful advisor answering questions only related to the given text
56-
57-
""".trimIndent(),
58-
),
59-
UserMessage.from(
60-
"""
61-
What does Blumblefang love? Text: ```${document.text()}```
62-
""".trimIndent(),
63-
),
51+
SystemMessage.from(
52+
"""
53+
You are helpful advisor answering questions only related to the given text
54+
""".trimIndent(),
55+
),
56+
UserMessage.from(
57+
"""
58+
What does Blumblefang love? Text: ```${document.text()}```
59+
""".trimIndent(),
6460
),
6561
)
6662

@@ -76,25 +72,23 @@ internal class ChatLanguageModelIT {
7672
val document = loadDocument("notes/blumblefang.txt", logger)
7773

7874
val response =
79-
model.chatAsync(
80-
ChatRequest
81-
.builder()
82-
.messages(
83-
listOf(
84-
SystemMessage.from(
85-
"""
86-
You are helpful advisor answering questions only related to the given text
87-
88-
""".trimIndent(),
89-
),
90-
UserMessage.from(
91-
"""
92-
What does Blumblefang love? Text: ```${document.text()}```
93-
""".trimIndent(),
94-
),
95-
),
96-
).responseFormat(ResponseFormat.TEXT),
97-
)
75+
model.chatAsync {
76+
messages +=
77+
SystemMessage.from(
78+
"""
79+
You are helpful advisor answering questions only related to the given text
80+
""".trimIndent(),
81+
)
82+
messages +=
83+
UserMessage.from(
84+
"""
85+
What does Blumblefang love? Text: ```${document.text()}```
86+
""".trimIndent(),
87+
)
88+
parameters {
89+
responseFormat(ResponseFormat.TEXT)
90+
}
91+
}
9892

9993
logger.info("Response: {}", response)
10094
assertThat(response).isNotNull()
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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.ChatLanguageModel
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: ChatLanguageModel
25+
26+
@Captor
27+
lateinit var chatRequestCaptor: org.mockito.ArgumentCaptor<ChatRequest>
28+
29+
@Mock
30+
lateinit var chatResponse: ChatResponse
31+
32+
@Test
33+
fun `Should call chatAsync`() {
34+
val temperature = 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(temperature)
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(temperature)
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)