Skip to content

Commit f935b73

Browse files
authored
Merge pull request #1572 from WebFuzzing/dictionary-loading
Dictionary loading
2 parents 8114779 + 46e3606 commit f935b73

11 files changed

Lines changed: 324 additions & 27 deletions

File tree

core/src/main/kotlin/org/evomaster/core/EMConfig.kt

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3015,6 +3015,12 @@ class EMConfig {
30153015
@Cfg("Enable the use of LLMs.")
30163016
var llm = false
30173017

3018+
@Experimental
3019+
@DependsOnTrueFor("llm")
3020+
@Cfg("The number of threads to use when making calls towards an LLM, in configured." +
3021+
" If connecting to Ollama, this value is ignored, and only 1 thread is used.")
3022+
var llmThreads = 4
3023+
30183024
@Experimental
30193025
@DependsOnTrueFor("llm")
30203026
@Cfg("LLM external service URL. If not specified, default will be based on the LLM provider.")
@@ -3107,32 +3113,33 @@ class EMConfig {
31073113
return (hours * 60 * 60) + (minutes * 60) + seconds
31083114
}
31093115

3110-
@Experimental
3116+
@Cfg("Enable the collection of response data, to feed new individuals based on field names matching.")
3117+
var useResponseDataPool = true
3118+
31113119
@Cfg("How much data elements, per key, can be stored in the Data Pool." +
31123120
" Once limit is reached, new old will replace old data. ")
31133121
@Min(1.0)
31143122
var maxSizeDataPool = 100
31153123

3116-
@Experimental
31173124
@Cfg("Threshold of Levenshtein Distance for key-matching in Data Pool")
31183125
@Min(0.0)
31193126
var thresholdDistanceForDataPool = 2
31203127

3121-
@Cfg("Enable the collection of response data, to feed new individuals based on field names matching.")
3122-
var useResponseDataPool = true
3123-
3124-
@Experimental
31253128
@Probability(false)
31263129
@Cfg("Specify the probability of using the data pool when sampling test cases." +
31273130
" This is for black-box (bb) mode")
31283131
var bbProbabilityUseDataPool = 0.8
31293132

3130-
@Experimental
31313133
@Probability(false)
31323134
@Cfg("Specify the probability of using the data pool when sampling test cases." +
31333135
" This is for white-box (wb) mode")
31343136
var wbProbabilityUseDataPool = 0.2
31353137

3138+
@Experimental
3139+
@Cfg("Specify if should use the pre-existing dictionary of values when sampling random string." +
3140+
" If so, those will be added to the data pool.")
3141+
var useDictionaryDataPool = false
3142+
31363143
@Cfg("Specify the naming strategy for test cases.")
31373144
var namingStrategy = defaultTestCaseNamingStrategy
31383145

core/src/main/kotlin/org/evomaster/core/Main.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.evomaster.core.AnsiColor.Companion.inYellow
1313
import org.evomaster.core.DocumentationLinks.EM_DOCKER_LINK
1414
import org.evomaster.core.DocumentationLinks.EM_ISSUES_LINK
1515
import org.evomaster.core.config.ConfigProblemException
16+
import org.evomaster.core.llm.service.LlmService
1617
import org.evomaster.core.logging.LoggingUtil
1718
import org.evomaster.core.output.TestSuiteCode
1819
import org.evomaster.core.output.TestSuiteSplitter
@@ -902,10 +903,17 @@ class Main {
902903
snapshotTimestamp: String ->
903904
writeTestsAsSnapshots(injector, solution, controllerInfo, snapshotTimestamp)
904905
}.also {
906+
/*
907+
TODO should have a better way to specify that some services need to be shutdown
908+
*/
905909
if (config.isEnabledHarvestingActualResponse()) {
906910
val hp = injector.getInstance(HarvestActualHttpWsResponseHandler::class.java)
907911
hp.shutdown()
908912
}
913+
if(config.llm){
914+
val llm = injector.getInstance(LlmService::class.java)
915+
llm.shutdown()
916+
}
909917
}
910918
}
911919

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ import kotlin.io.path.Path
1717

1818
object DictionaryCreator {
1919

20-
private data class FieldInfo(
21-
val name: String,
22-
val description: String?
23-
)
24-
2520
@JvmStatic
2621
fun main(args: Array<String>) {
2722

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.evomaster.core.llm
2+
3+
class DictionarySearchResult(
4+
5+
val data: Map<String, Set<String>>,
6+
7+
val missing: Set<String>
8+
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package org.evomaster.core.llm
2+
3+
data class FieldInfo(
4+
val name: String,
5+
val description: String?
6+
)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package org.evomaster.core.llm.service
2+
3+
import com.fasterxml.jackson.core.type.TypeReference
4+
import com.fasterxml.jackson.databind.ObjectMapper
5+
import org.evomaster.core.EMConfig
6+
import org.evomaster.core.llm.DictionarySearchResult
7+
import org.evomaster.core.llm.FieldInfo
8+
import org.evomaster.core.logging.LoggingUtil
9+
import org.evomaster.core.search.service.DataPool
10+
import org.slf4j.Logger
11+
import org.slf4j.LoggerFactory
12+
import javax.inject.Inject
13+
14+
15+
class DictionaryService {
16+
17+
companion object {
18+
private val log: Logger = LoggerFactory.getLogger(DictionaryService::class.java)
19+
private const val location = "/llm_dictionary.jsonl"
20+
21+
/**
22+
* WARNING: possibly quite expensive... should only be used for debugging/testing/statistics
23+
*/
24+
fun loadAll() : Map<String, Set<String>> {
25+
26+
val data = mutableMapOf<String, Set<String>>()
27+
val mapper = ObjectMapper()
28+
29+
val reader = DictionaryService::class.java.getResourceAsStream(location)!!.bufferedReader()
30+
31+
reader.forEachLine { line ->
32+
addLineEntry(mapper, line, data)
33+
}
34+
35+
return data
36+
}
37+
38+
39+
/**
40+
* Load the dictionary from file, and scan it for the given name entries.
41+
* Considering the large size of the dictionary, this functions tries to be efficient.
42+
* Still, it can be quite expensive, and should be called ideally only once.
43+
*/
44+
fun searchForNames(names: Collection<String>): DictionarySearchResult {
45+
46+
if(names.isEmpty()) {
47+
throw IllegalArgumentException("No name to find is specified")
48+
}
49+
50+
val found = mutableMapOf<String, Set<String>>()
51+
val missing = mutableSetOf<String>()
52+
val result = DictionarySearchResult(found, missing)
53+
val mapper = ObjectMapper()
54+
55+
val reader = DictionaryService::class.java.getResourceAsStream(location)!!.bufferedReader()
56+
57+
val toFind = names.toList().sorted()
58+
var index = 0
59+
60+
reader.lineSequence()
61+
.takeWhile { index < toFind.size }
62+
.forEach { line ->
63+
64+
//let's avoid parsing whole line with Jackson if we do not have a name-match
65+
val startQuote = line.indexOf('"')
66+
val endQuote = line.indexOf('"', startQuote+1)
67+
val x = line.substring(startQuote+1, endQuote)
68+
69+
var handled = false
70+
71+
while(!handled && index < toFind.size) {
72+
val f = toFind[index]
73+
74+
if (f == x) {
75+
addLineEntry(mapper, line, found)
76+
index++
77+
handled = true
78+
} else if (x < f) {
79+
//nothing to do. f is not found, but could be found in the next lines
80+
//recall names and dictionary is alphabetically sorted
81+
handled = true
82+
} else {
83+
missing.add(f)
84+
index++
85+
}
86+
}
87+
}
88+
89+
// in case any asked name come alphabetically after the last element in the dictionary
90+
while(index < toFind.size) {
91+
missing.add(toFind[index])
92+
index++
93+
}
94+
95+
//would not hold if there are errors in parsing... but then if so we should remove those from dictionary!!!
96+
assert(names.size == result.data.size + result.missing.size)
97+
return result
98+
}
99+
100+
private fun addLineEntry(
101+
mapper: ObjectMapper,
102+
line: String,
103+
found: MutableMap<String, Set<String>>
104+
) {
105+
val node = mapper.readTree(line)
106+
if (!node.isObject) {
107+
log.warn("Not an object: $line")
108+
} else {
109+
node.fields().forEach { field ->
110+
if (!field.value.isArray) {
111+
log.warn("Not containing an array: $line")
112+
} else {
113+
found[field.key] = mapper.convertValue(field.value, object : TypeReference<Set<String>>() {})
114+
}
115+
}
116+
}
117+
}
118+
}
119+
120+
121+
122+
@Inject
123+
private lateinit var config: EMConfig
124+
125+
@Inject
126+
private lateinit var llmService: LlmService
127+
128+
@Inject
129+
private lateinit var dataPool : DataPool
130+
131+
132+
/**
133+
* Update the data pool with all the info from dictionary based on the input field's names.
134+
* If some names are not in the dictionary, if LLM is enabled then it will be queried,
135+
* including using the names' descriptions (if any is provided)
136+
*/
137+
fun updatePoolFromDictionary(fields: Collection<FieldInfo>) {
138+
if(!isActive()){
139+
throw IllegalStateException("Dictionary service is not active")
140+
}
141+
142+
//TODO check how expensive this is... put on thread if too expensive
143+
144+
val result = searchForNames(fields.map { it.name })
145+
146+
result.data.entries.forEach { entry ->
147+
entry.value.forEach { exm -> dataPool.addValue(entry.key, exm) }
148+
}
149+
150+
if(config.llm && result.missing.isNotEmpty()) {
151+
LoggingUtil.getInfoLogger().info("Going to make ${result.missing.size} calls to LLM to obtain new input data" +
152+
" based on field names and descriptions")
153+
154+
val toInfer = fields.filter{ result.missing.contains(it.name)}
155+
156+
llmService.askForNewExamples(toInfer){ name, examples ->
157+
examples.forEach { ex -> dataPool.addValue(name, ex) }
158+
}
159+
}
160+
}
161+
162+
163+
fun isActive() : Boolean {
164+
return config.useDictionaryDataPool
165+
}
166+
}

core/src/main/kotlin/org/evomaster/core/llm/service/LlmService.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package org.evomaster.core.llm.service
22

3+
import com.fasterxml.jackson.core.type.TypeReference
4+
import com.fasterxml.jackson.databind.ObjectMapper
35
import dev.langchain4j.model.chat.ChatModel
46
import dev.langchain4j.model.chat.StreamingChatModel
57
import org.evomaster.core.EMConfig
68
import org.evomaster.core.config.ConfigProblemException
9+
import org.evomaster.core.llm.FieldInfo
10+
import org.evomaster.core.llm.LlmProvider
711
import org.evomaster.core.llm.LlmSupport
12+
import org.evomaster.core.llm.Prompts
13+
import java.util.concurrent.ExecutorService
14+
import java.util.concurrent.Executors
815
import java.util.concurrent.Future
916
import javax.annotation.PostConstruct
1017
import javax.inject.Inject
@@ -18,6 +25,14 @@ class LlmService {
1825

1926
private lateinit var syncModel: ChatModel
2027

28+
/**
29+
* We can make several requests in parallel toward an LLM.
30+
* In the end, these are I/O bound, and could take fews seconds each.
31+
* So make sense to do in parallel and asynchronously, especially when we do not need to wait
32+
* for the responses.
33+
*/
34+
private lateinit var executor: ExecutorService
35+
2136
@PostConstruct
2237
private fun initService() {
2338

@@ -47,6 +62,13 @@ class LlmService {
4762
}catch (e: Exception){
4863
throw ConfigProblemException("Failed to connect and initialize the chosen LLM: ${e.message}")
4964
}
65+
66+
val n = if(config.llmProvider == LlmProvider.OLLAMA) 1 else config.llmThreads
67+
executor = Executors.newFixedThreadPool(n)
68+
}
69+
70+
fun shutdown() {
71+
executor.shutdown()
5072
}
5173

5274
private fun checkUsingLLM(){
@@ -64,4 +86,44 @@ class LlmService {
6486
fun chat(userMessage: String) : String{
6587
return LlmSupport.chat(syncModel, "", userMessage)
6688
}
89+
90+
/**
91+
* Query the LLM for input data examples, based on parameter name and optional description.
92+
* These calls will be made in parallel, for each [fields] entry.
93+
* When a task is completed, the [callback] is executed with the collected examples.
94+
*/
95+
fun askForNewExamples(fields: Collection<FieldInfo>, callback: (name: String, examples: Collection<String>) -> Unit){
96+
97+
fields.toList()
98+
.sortedBy { it.name }
99+
.forEach { entry ->
100+
executor.submit { askForExamplesTask(entry.name, entry.description, callback) }
101+
}
102+
//here we do not wait... updates are done asynchronously
103+
}
104+
105+
private fun askForExamplesTask(
106+
name: String,
107+
description: String?,
108+
callback: (name: String, examples: Collection<String>) -> Unit
109+
){
110+
/*
111+
Note: this is the same approach used to build the dictionary
112+
*/
113+
val mapper = ObjectMapper()
114+
val prompt = Prompts.getPromptForNameDescription(name, description)
115+
var result = LlmSupport.chat(syncModel, prompt.first, prompt.second)
116+
val list = try {
117+
mapper.readValue(result, object : TypeReference<List<String>>() {})
118+
} catch (e: Exception) {
119+
try {
120+
val failed = Prompts.getPromptForFailedName(e.toString())
121+
result = LlmSupport.chat(syncModel, failed.first, failed.second)
122+
mapper.readValue(result, object : TypeReference<List<String>>() {})
123+
} catch (e: Exception) {
124+
throw e
125+
}
126+
}
127+
callback(name, list)
128+
}
67129
}

core/src/main/kotlin/org/evomaster/core/problem/enterprise/service/EnterpriseModule.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.evomaster.core.problem.enterprise.service
22

33
import com.google.inject.AbstractModule
4+
import org.evomaster.core.llm.service.DictionaryService
45
import org.evomaster.core.llm.service.LlmService
56
import org.evomaster.core.problem.security.service.SSRFAnalyser
67
import org.evomaster.core.problem.security.service.HttpCallbackVerifier
@@ -16,6 +17,9 @@ abstract class EnterpriseModule : AbstractModule() {
1617
bind(LlmService::class.java)
1718
.asEagerSingleton()
1819

20+
bind(DictionaryService::class.java)
21+
.asEagerSingleton()
22+
1923
bind(SSRFAnalyser::class.java)
2024
.asEagerSingleton()
2125

0 commit comments

Comments
 (0)