Skip to content

Commit dee2ef6

Browse files
committed
feat(grep): truncate search results to 2000 characters #453
Limit grep tool output to 2000 characters and indicate when results are truncated to prevent excessively long responses.
1 parent 359ddde commit dee2ef6

File tree

1 file changed

+70
-78
lines changed
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/impl

1 file changed

+70
-78
lines changed

mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/tool/impl/GrepTool.kt

Lines changed: 70 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,37 @@ data class GrepParams(
1818
* The regular expression pattern to search for in file contents
1919
*/
2020
val pattern: String,
21-
21+
2222
/**
2323
* The directory to search in (optional, defaults to project root)
2424
*/
2525
val path: String? = null,
26-
26+
2727
/**
2828
* File pattern to include in the search (e.g. "*.kt", "*.{ts,js}")
2929
*/
3030
val include: String? = null,
31-
31+
3232
/**
3333
* File pattern to exclude from the search
3434
*/
3535
val exclude: String? = null,
36-
36+
3737
/**
3838
* Whether the search should be case-sensitive
3939
*/
4040
val caseSensitive: Boolean = false,
41-
41+
4242
/**
4343
* Maximum number of matches to return
4444
*/
4545
val maxMatches: Int = 100,
46-
46+
4747
/**
4848
* Number of context lines to show before and after each match
4949
*/
5050
val contextLines: Int = 0,
51-
51+
5252
/**
5353
* Whether to search recursively in subdirectories
5454
*/
@@ -133,29 +133,27 @@ class GrepInvocation(
133133
tool: GrepTool,
134134
private val fileSystem: ToolFileSystem
135135
) : BaseToolInvocation<GrepParams, ToolResult>(params, tool) {
136-
136+
137137
override fun getDescription(): String {
138138
val searchPath = params.path ?: "project root"
139139
val includeDesc = params.include?.let { " (include: $it)" } ?: ""
140140
val excludeDesc = params.exclude?.let { " (exclude: $it)" } ?: ""
141141
return "Search for pattern '${params.pattern}' in $searchPath$includeDesc$excludeDesc"
142142
}
143-
143+
144144
override fun getToolLocations(): List<ToolLocation> {
145145
val searchPath = params.path ?: fileSystem.getProjectPath() ?: "."
146146
return listOf(ToolLocation(searchPath, LocationType.DIRECTORY))
147147
}
148-
148+
149149
override suspend fun execute(context: ToolExecutionContext): ToolResult {
150150
return ToolErrorUtils.safeExecute(ToolErrorType.INVALID_PATTERN) {
151151
val searchPath = params.path ?: fileSystem.getProjectPath() ?: "."
152-
153-
// Validate search path exists
152+
154153
if (!fileSystem.exists(searchPath)) {
155154
throw ToolException("Search path not found: $searchPath", ToolErrorType.DIRECTORY_NOT_FOUND)
156155
}
157-
158-
// Create regex pattern
156+
159157
val regex = try {
160158
if (params.caseSensitive) {
161159
Regex(params.pattern)
@@ -165,25 +163,23 @@ class GrepInvocation(
165163
} catch (e: Exception) {
166164
throw ToolException("Invalid regex pattern: ${params.pattern}", ToolErrorType.INVALID_PATTERN)
167165
}
168-
169-
// Find files to search
166+
170167
val filesToSearch = findFilesToSearch(searchPath)
171-
172-
// Search for matches
168+
173169
val matches = mutableListOf<GrepMatch>()
174170
var totalMatches = 0
175-
171+
176172
for (file in filesToSearch) {
177173
if (totalMatches >= params.maxMatches) break
178-
174+
179175
val fileMatches = searchInFile(file, regex)
180176
matches.addAll(fileMatches.take(params.maxMatches - totalMatches))
181177
totalMatches += fileMatches.size
182178
}
183-
179+
184180
// Format results
185181
val resultText = formatResults(matches, filesToSearch.size)
186-
182+
187183
val metadata = mapOf(
188184
"pattern" to params.pattern,
189185
"search_path" to searchPath,
@@ -194,66 +190,66 @@ class GrepInvocation(
194190
"include_pattern" to (params.include ?: ""),
195191
"exclude_pattern" to (params.exclude ?: "")
196192
)
197-
193+
198194
ToolResult.Success(resultText, metadata)
199195
}
200196
}
201-
197+
202198
private fun findFilesToSearch(searchPath: String): List<String> {
203199
val files = mutableListOf<String>()
204-
200+
205201
fun collectFiles(path: String) {
206202
val pathFiles = fileSystem.listFiles(path)
207-
203+
208204
for (file in pathFiles) {
209205
val fileInfo = fileSystem.getFileInfo(file)
210-
206+
211207
if (fileInfo?.isDirectory == true && params.recursive) {
212208
collectFiles(file)
213209
} else if (fileInfo?.isDirectory == false) {
214210
// Check include/exclude patterns
215211
val fileName = file.substringAfterLast('/')
216-
212+
217213
val shouldInclude = params.include?.let { pattern ->
218214
matchesGlobPattern(fileName, pattern)
219215
} ?: true
220-
216+
221217
val shouldExclude = params.exclude?.let { pattern ->
222218
matchesGlobPattern(fileName, pattern)
223219
} ?: false
224-
220+
225221
if (shouldInclude && !shouldExclude) {
226222
files.add(file)
227223
}
228224
}
229225
}
230226
}
231-
227+
232228
collectFiles(searchPath)
233229
return files.sorted()
234230
}
235-
231+
236232
private suspend fun searchInFile(filePath: String, regex: Regex): List<GrepMatch> {
237233
val matches = mutableListOf<GrepMatch>()
238-
234+
239235
try {
240236
val content = fileSystem.readFile(filePath) ?: return emptyList()
241237
val lines = content.lines()
242-
238+
243239
lines.forEachIndexed { index, line ->
244240
val matchResults = regex.findAll(line)
245-
241+
246242
for (matchResult in matchResults) {
247243
val contextBefore = if (params.contextLines > 0) {
248244
val startIndex = (index - params.contextLines).coerceAtLeast(0)
249245
lines.subList(startIndex, index)
250246
} else emptyList()
251-
247+
252248
val contextAfter = if (params.contextLines > 0) {
253249
val endIndex = (index + params.contextLines + 1).coerceAtMost(lines.size)
254250
lines.subList(index + 1, endIndex)
255251
} else emptyList()
256-
252+
257253
matches.add(
258254
GrepMatch(
259255
file = filePath,
@@ -270,54 +266,49 @@ class GrepInvocation(
270266
} catch (e: Exception) {
271267
// Skip files that can't be read
272268
}
273-
269+
274270
return matches
275271
}
276-
272+
277273
private fun formatResults(matches: List<GrepMatch>, filesSearched: Int): String {
278274
if (matches.isEmpty()) {
279275
return "No matches found for pattern '${params.pattern}' in $filesSearched files."
280276
}
281-
282-
val result = StringBuilder()
283-
result.appendLine("Found ${matches.size} matches for pattern '${params.pattern}' in $filesSearched files:")
284-
result.appendLine()
285-
277+
278+
val sb = StringBuilder()
279+
sb.appendLine("Found ${matches.size} matches for pattern '${params.pattern}' in $filesSearched files:")
280+
sb.appendLine()
281+
286282
var currentFile = ""
287-
283+
val maxChars = 2000
284+
288285
for (match in matches) {
286+
if (sb.length > maxChars) {
287+
sb.appendLine()
288+
sb.appendLine("... (results truncated to $maxChars characters)")
289+
break
290+
}
291+
289292
if (match.file != currentFile) {
290-
if (currentFile.isNotEmpty()) result.appendLine()
291-
result.appendLine("File: ${match.file}")
293+
if (currentFile.isNotEmpty()) sb.appendLine()
294+
sb.appendLine("### ${match.file}")
292295
currentFile = match.file
293296
}
294-
295-
// Show context before
296-
match.contextBefore.forEachIndexed { index, contextLine ->
297-
val lineNum = match.lineNumber - match.contextBefore.size + index
298-
result.appendLine(" $lineNum: $contextLine")
297+
298+
match.contextBefore.forEach { contextLine ->
299+
sb.appendLine(contextLine)
299300
}
300-
301-
// Show the match line with highlighting
302-
val line = match.line
303-
val beforeMatch = line.substring(0, match.matchStart)
304-
val matchText = line.substring(match.matchStart, match.matchEnd)
305-
val afterMatch = line.substring(match.matchEnd)
306-
307-
result.appendLine("${match.lineNumber}: $beforeMatch**$matchText**$afterMatch")
308-
309-
// Show context after
310-
match.contextAfter.forEachIndexed { index, contextLine ->
311-
val lineNum = match.lineNumber + index + 1
312-
result.appendLine(" $lineNum: $contextLine")
301+
302+
sb.appendLine(match.line)
303+
304+
match.contextAfter.forEach { contextLine ->
305+
sb.appendLine(contextLine)
313306
}
314-
315-
if (params.contextLines > 0) result.appendLine()
316307
}
317-
318-
return result.toString().trim()
308+
309+
return sb.toString()
319310
}
320-
311+
321312
private fun matchesGlobPattern(fileName: String, pattern: String): Boolean {
322313
// Simple glob pattern matching
323314
val regexPattern = pattern
@@ -327,7 +318,7 @@ class GrepInvocation(
327318
.replace("{", "(")
328319
.replace("}", ")")
329320
.replace(",", "|")
330-
321+
331322
return fileName.matches(Regex(regexPattern))
332323
}
333324
}
@@ -338,25 +329,26 @@ class GrepInvocation(
338329
class GrepTool(
339330
private val fileSystem: ToolFileSystem
340331
) : BaseExecutableTool<GrepParams, ToolResult>() {
341-
332+
342333
override val name: String = "grep"
343-
override val description: String = """Searches for a regular expression pattern within the content of files in a specified directory (or current working directory). Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.""".trimIndent()
344-
334+
override val description: String =
335+
"""Searches for a regular expression pattern within the content of files in a specified directory (or current working directory). Can filter files by a glob pattern. Returns the lines containing matches, along with their file paths and line numbers.""".trimIndent()
336+
345337
override val metadata: ToolMetadata = ToolMetadata(
346338
displayName = "Search Content",
347339
tuiEmoji = "🔍",
348340
composeIcon = "search",
349341
category = ToolCategory.Search,
350342
schema = GrepSchema
351343
)
352-
344+
353345
override fun getParameterClass(): String = GrepParams::class.simpleName ?: "GrepParams"
354-
355-
override fun createToolInvocation(params: GrepParams): ToolInvocation<GrepParams, ToolResult> {
346+
347+
public override fun createToolInvocation(params: GrepParams): ToolInvocation<GrepParams, ToolResult> {
356348
validateParameters(params)
357349
return GrepInvocation(params, this, fileSystem)
358350
}
359-
351+
360352
private fun validateParameters(params: GrepParams) {
361353
if (params.pattern.isBlank()) {
362354
throw ToolException("Search pattern cannot be empty", ToolErrorType.MISSING_REQUIRED_PARAMETER)

0 commit comments

Comments
 (0)