Skip to content

Commit 1dae2ba

Browse files
committed
basic mock scaffolding for LLM use
1 parent f935b73 commit 1dae2ba

7 files changed

Lines changed: 132 additions & 2 deletions

File tree

core/src/main/kotlin/org/evomaster/core/llm/LlmProvider.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ enum class LlmProvider {
55
OPENAI,
66
DEEPSEEK, // OpenAI-compatible
77
OLLAMA,
8-
AZURE_OPENAI
8+
AZURE_OPENAI,
9+
MOCK
910
}

core/src/main/kotlin/org/evomaster/core/llm/LlmSupport.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import dev.langchain4j.model.ollama.OllamaChatModel
1414
import dev.langchain4j.model.ollama.OllamaStreamingChatModel
1515
import dev.langchain4j.model.openai.OpenAiChatModel
1616
import dev.langchain4j.model.openai.OpenAiStreamingChatModel
17+
import org.evomaster.core.llm.mock.MockChatModel
18+
import org.evomaster.core.llm.mock.MockStreamingChatModel
1719
import java.util.concurrent.CompletableFuture
1820

1921

@@ -35,6 +37,13 @@ object LlmSupport {
3537

3638
}
3739

40+
fun chat(model: ChatModel, userMessage: String): String {
41+
42+
val u = UserMessage.userMessage(userMessage)
43+
44+
return model.chat(listOf(u)).aiMessage().text()
45+
}
46+
3847
fun chat(model: ChatModel, systemMessage: String, userMessage: String): String {
3948

4049
val s = SystemMessage.systemMessage(systemMessage)
@@ -76,6 +85,8 @@ object LlmSupport {
7685
*/
7786

7887
return when (provider) {
88+
LlmProvider.MOCK -> MockChatModel()
89+
7990
LlmProvider.OPENAI -> OpenAiChatModel.builder()
8091
.apiKey(apiKey ?: error("API key required for OpenAI"))
8192
.baseUrl(url ?: "https://api.openai.com/v1")
@@ -138,6 +149,8 @@ object LlmSupport {
138149
*/
139150

140151
return when (provider) {
152+
LlmProvider.MOCK -> MockStreamingChatModel()
153+
141154
LlmProvider.OPENAI -> OpenAiStreamingChatModel.builder()
142155
.apiKey(apiKey ?: error("API key required for OpenAI"))
143156
.baseUrl(url ?: "https://api.openai.com/v1")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.evomaster.core.llm.mock
2+
3+
class LlmMockResponse(
4+
5+
/**
6+
* The response that will be returned by the LLM
7+
*/
8+
val response: String,
9+
10+
/**
11+
* Lambda that, given as input a request from the client, decides whether this response should be returned
12+
*/
13+
val matcher: (String) -> Boolean
14+
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.evomaster.core.llm.mock
2+
3+
import dev.langchain4j.data.message.AiMessage
4+
import dev.langchain4j.model.chat.ChatModel
5+
import dev.langchain4j.model.chat.ChatRequestOptions
6+
import dev.langchain4j.model.chat.request.ChatRequest
7+
import dev.langchain4j.model.chat.response.ChatResponse
8+
9+
class MockChatModel : ChatModel {
10+
11+
companion object {
12+
13+
/*
14+
WARNING
15+
Global mutable state... but only used for testing.
16+
But out of the box would prevent parallel tests in same process, which we don't anyway,
17+
so shouldn't be a big deal
18+
*/
19+
private val responses : MutableList<LlmMockResponse> = mutableListOf()
20+
21+
/**
22+
* Clear the cache of mocked responses.
23+
* Important to avoid dependencies among tests.
24+
* Recall this is using mutable static state.
25+
*/
26+
fun reset(){
27+
responses.clear()
28+
}
29+
30+
/**
31+
* Specify the [response] that will be returned by the LLM,
32+
* based on [matcher] that, given as input a request from the client,
33+
* decides whether this response should be returned
34+
*/
35+
fun mockResponse(response: String, matcher: (String) -> Boolean){
36+
responses.add(LlmMockResponse(response, matcher))
37+
}
38+
}
39+
40+
/**
41+
* Main method that needs to be mocked.
42+
* All other methods seem to call this one.
43+
*/
44+
override fun chat(chatRequest: ChatRequest, options: ChatRequestOptions) : ChatResponse {
45+
46+
val text = chatRequest.messages().joinToString { it.toString() }
47+
48+
val res = responses.find { mock -> mock.matcher(text) }?.response
49+
?: "Hei! You forgot to setup the mock for me"
50+
51+
return ChatResponse.builder().aiMessage(AiMessage.from(res)).build()
52+
}
53+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.evomaster.core.llm.mock
2+
3+
import dev.langchain4j.model.chat.ChatRequestOptions
4+
import dev.langchain4j.model.chat.StreamingChatModel
5+
import dev.langchain4j.model.chat.request.ChatRequest
6+
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler
7+
8+
/**
9+
* WARNING:
10+
* we wrote the code for it, but in the end we did not use StreamingChatModel.
11+
* TODO If one day we do, then we need to update this class if we want to have tests using it.
12+
*/
13+
class MockStreamingChatModel : StreamingChatModel {
14+
15+
override fun chat(request: ChatRequest?, options: ChatRequestOptions?, handler: StreamingChatResponseHandler?){
16+
17+
throw NotImplementedError()
18+
}
19+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.evomaster.core.llm
2+
3+
import org.evomaster.core.llm.mock.MockChatModel
4+
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.Test
6+
7+
class LlmSupportMockTest {
8+
9+
@Test
10+
fun testBasePrompt(){
11+
val model = LlmSupport.createModel(LlmProvider.MOCK)
12+
13+
val firstPrompt = """
14+
Is A the first letter in the English alphabet? Answer this question with either a "YES" or a "NO".
15+
Do not add anything else in your response.
16+
""".trimIndent()
17+
18+
val secondPrompt = """
19+
Is Sweden the capital of Norway?
20+
""".trimIndent()
21+
22+
MockChatModel.reset()
23+
MockChatModel.mockResponse("YES"){ it.contains("English")}
24+
MockChatModel.mockResponse("NO"){ it.contains("Norway")}
25+
26+
27+
assertEquals("YES", LlmSupport.chat(model, firstPrompt))
28+
assertEquals("NO", LlmSupport.chat(model, secondPrompt))
29+
}
30+
}

docs/options.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ There are 3 types of options:
309309
|`llm`| __Boolean__. Enable the use of LLMs. *Default value*: `false`.|
310310
|`llmApiKey`| __String__. API KEY needed to authenticated toward the chosen LLM provider. *Depends on*: `llm=true`. *Default value*: `null`.|
311311
|`llmName`| __String__. LLM name. If not specified, default will be based on the LLM provider. *Depends on*: `llm=true`. *Default value*: `null`.|
312-
|`llmProvider`| __Enum__. Provider for the LLM. This could be a local one (e.g., run through Ollama), or a remote one like OpenAI. *Depends on*: `llm=true`. *Valid values*: `OPENAI, DEEPSEEK, OLLAMA, AZURE_OPENAI`. *Default value*: `OLLAMA`.|
312+
|`llmProvider`| __Enum__. Provider for the LLM. This could be a local one (e.g., run through Ollama), or a remote one like OpenAI. *Depends on*: `llm=true`. *Valid values*: `OPENAI, DEEPSEEK, OLLAMA, AZURE_OPENAI, MOCK`. *Default value*: `OLLAMA`.|
313313
|`llmTemperature`| __Double__. Temperature parameter for LLM. *Constraints*: `min=0.0, max=2.0`. *Depends on*: `llm=true`. *Default value*: `0.3`.|
314314
|`llmThreads`| __Int__. The number of threads to use when making calls towards an LLM, in configured. If connecting to Ollama, this value is ignored, and only 1 thread is used. *Depends on*: `llm=true`. *Default value*: `4`.|
315315
|`llmTimeoutSeconds`| __Long__. How long to wait for LLM's responses. *Constraints*: `min=0.0`. *Depends on*: `llm=true`. *Default value*: `60`.|

0 commit comments

Comments
 (0)