mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
search tools now support regex, reading line numbers, etc
This commit is contained in:
parent
2aae01f3b6
commit
b440c8732f
5 changed files with 111 additions and 37 deletions
|
|
@ -1198,7 +1198,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
// else search codebase for `target`
|
||||
let uris: URI[] = []
|
||||
try {
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, pageNumber: 0 })
|
||||
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, include: null, pageNumber: 0 })
|
||||
uris = result.uris
|
||||
} catch (e) {
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -541,7 +541,7 @@ export const SelectedFiles = (
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center flex-wrap text-left relative gap-x-0.5 gap-y-1'>
|
||||
<div className='flex items-center flex-wrap text-left relative gap-x-0.5 gap-y-1 pb-0.5'>
|
||||
|
||||
{allSelections.map((selection, i) => {
|
||||
|
||||
|
|
@ -1178,7 +1178,7 @@ const loadingTitleWrapper = (item: React.ReactNode) => {
|
|||
}
|
||||
const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file'
|
||||
const titleOfToolName = {
|
||||
'view_file_contents': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
|
||||
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
|
||||
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||
'get_dir_structure': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
|
||||
'search_pathnames_only': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') },
|
||||
|
|
@ -1205,8 +1205,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
|
|||
return '';
|
||||
}
|
||||
|
||||
if (toolName === 'view_file_contents') {
|
||||
const toolParams = _toolParams as ToolCallParams['view_file_contents']
|
||||
if (toolName === 'read_file') {
|
||||
const toolParams = _toolParams as ToolCallParams['read_file']
|
||||
return getBasename(toolParams.uri.fsPath);
|
||||
} else if (toolName === 'ls_dir') {
|
||||
const toolParams = _toolParams as ToolCallParams['ls_dir']
|
||||
|
|
@ -1373,7 +1373,7 @@ type ToolComponent<T extends ToolName,> = {
|
|||
}
|
||||
|
||||
const toolNameToComponent: { [T in ToolName]: ToolComponent<T> } = {
|
||||
'view_file_contents': {
|
||||
'read_file': {
|
||||
requestWrapper: null,
|
||||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
|
|
|
|||
|
|
@ -55,7 +55,9 @@ const validateJSON = (s: string): { [s: string]: unknown } => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
return !u || u === 'null' || u === 'undefined'
|
||||
}
|
||||
|
||||
const validateStr = (argName: string, value: unknown) => {
|
||||
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
|
||||
|
|
@ -64,13 +66,24 @@ const validateStr = (argName: string, value: unknown) => {
|
|||
|
||||
|
||||
// We are NOT checking to make sure in workspace
|
||||
// TODO!!!! check to make sure folder/file exists
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
|
||||
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
const validateOptionalURI = (uriStr: unknown) => {
|
||||
if (isFalsy(uriStr)) return null
|
||||
return validateURI(uriStr)
|
||||
}
|
||||
|
||||
const validateOptionalStr = (argName: string, str: unknown) => {
|
||||
if (isFalsy(str)) return null
|
||||
return validateStr(argName, str)
|
||||
}
|
||||
|
||||
|
||||
const validatePageNum = (pageNumberUnknown: unknown) => {
|
||||
if (!pageNumberUnknown) return 1
|
||||
const parsedInt = Number.parseInt(pageNumberUnknown + '')
|
||||
|
|
@ -79,6 +92,20 @@ const validatePageNum = (pageNumberUnknown: unknown) => {
|
|||
return parsedInt
|
||||
}
|
||||
|
||||
const validateNumber = (numStr: unknown, opts: { default: number | null }) => {
|
||||
if (typeof numStr === 'number')
|
||||
return numStr
|
||||
if (isFalsy(numStr)) return opts.default
|
||||
|
||||
if (typeof numStr === 'string') {
|
||||
const parsedInt = Number.parseInt(numStr + '')
|
||||
if (!Number.isInteger(parsedInt)) return opts.default
|
||||
return parsedInt
|
||||
}
|
||||
|
||||
return opts.default
|
||||
}
|
||||
|
||||
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
|
||||
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
|
||||
const params = paramsUnknown
|
||||
|
|
@ -92,12 +119,15 @@ const validateProposedTerminalId = (terminalIdUnknown: unknown) => {
|
|||
return terminalId
|
||||
}
|
||||
|
||||
const validateWaitForCompletion = (b: unknown) => {
|
||||
const validateBoolean = (b: unknown, opts: { default: boolean }) => {
|
||||
if (typeof b === 'string') {
|
||||
if (b === 'true') return true
|
||||
if (b === 'false') return false
|
||||
}
|
||||
return true // default is true
|
||||
if (typeof b === 'boolean') {
|
||||
return b
|
||||
}
|
||||
return opts.default
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -139,14 +169,17 @@ export class ToolsService implements IToolsService {
|
|||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
view_file_contents: async (params: string) => {
|
||||
read_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { uri, pageNumber }
|
||||
const startLine = validateNumber(startLineUnknown, { default: null })
|
||||
const endLine = validateNumber(endLineUnknown, { default: null })
|
||||
|
||||
return { uri, startLine, endLine, pageNumber }
|
||||
},
|
||||
ls_dir: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
|
|
@ -164,22 +197,35 @@ export class ToolsService implements IToolsService {
|
|||
},
|
||||
search_pathnames_only: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
const {
|
||||
query: queryUnknown,
|
||||
include: includeUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
const include = validateOptionalStr('include', includeUnknown)
|
||||
|
||||
return { queryStr, pageNumber }
|
||||
return { queryStr, include, pageNumber }
|
||||
|
||||
},
|
||||
search_files: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
const {
|
||||
query: queryUnknown,
|
||||
searchInFolder: searchInFolderUnknown,
|
||||
isRegex: isRegexUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { queryStr, pageNumber }
|
||||
const searchInFolder = validateOptionalURI(searchInFolderUnknown)
|
||||
const isRegex = validateBoolean(isRegexUnknown, { default: false })
|
||||
|
||||
return { queryStr, searchInFolder, isRegex, pageNumber }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
|
@ -216,7 +262,7 @@ export class ToolsService implements IToolsService {
|
|||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
|
||||
const command = validateStr('command', commandUnknown)
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown)
|
||||
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
|
||||
return { command, proposedTerminalId, waitForCompletion }
|
||||
},
|
||||
|
||||
|
|
@ -224,16 +270,25 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
|
||||
this.callTool = {
|
||||
view_file_contents: async ({ uri, pageNumber }) => {
|
||||
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
const { model } = await voidModelService.getModelSafe(uri)
|
||||
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
|
||||
const readFileContents = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
let contents: string
|
||||
if (startLine === null && endLine === null) {
|
||||
contents = model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
else {
|
||||
const startLineNumber = startLine === null ? 1 : startLine
|
||||
const endLineNumber = endLine === null ? model.getLineCount() : endLine
|
||||
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (contents.length - 1) - toIdx >= 1
|
||||
|
||||
return { result: { fileContents, hasNextPage } }
|
||||
},
|
||||
|
|
@ -250,9 +305,11 @@ export class ToolsService implements IToolsService {
|
|||
return { result: { str } }
|
||||
},
|
||||
|
||||
search_pathnames_only: async ({ queryStr, pageNumber }) => {
|
||||
search_pathnames_only: async ({ queryStr, include, pageNumber }) => {
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: include ?? undefined,
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -266,11 +323,15 @@ export class ToolsService implements IToolsService {
|
|||
return { result: { uris, hasNextPage } }
|
||||
},
|
||||
|
||||
search_files: async ({ queryStr, pageNumber }) => {
|
||||
search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
const searchFolders = searchInFolder === null ?
|
||||
workspaceContextService.getWorkspace().folders.map(f => f.uri)
|
||||
: [searchInFolder]
|
||||
|
||||
const query = queryBuilder.text({
|
||||
pattern: queryStr,
|
||||
isRegExp: true,
|
||||
}, workspaceContextService.getWorkspace().folders.map(f => f.uri))
|
||||
isRegExp: isRegex,
|
||||
}, searchFolders)
|
||||
|
||||
const data = await searchService.textSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -333,7 +394,7 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
// given to the LLM after the call
|
||||
this.stringOfResult = {
|
||||
view_file_contents: (params, result) => {
|
||||
read_file: (params, result) => {
|
||||
return result.fileContents + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
ls_dir: (params, result) => {
|
||||
|
|
|
|||
|
|
@ -48,14 +48,23 @@ const uriParam = (object: string) => ({
|
|||
uri: { type: 'string', description: `The FULL path to the ${object}.` }
|
||||
})
|
||||
|
||||
|
||||
const searchParams = {
|
||||
searchInFolder: { type: 'string', description: 'Only search files in this given folder. Leave as empty to search all available files.' },
|
||||
isRegex: { type: 'string', description: 'Whether to treat the query as a regular expression. Default is "false".' },
|
||||
} as const
|
||||
|
||||
|
||||
export const voidTools = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
view_file_contents: {
|
||||
name: 'view_file_contents',
|
||||
read_file: {
|
||||
name: 'read_file',
|
||||
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
startLine: { type: 'string', description: 'Line to start reading from. Default is "null", treated as 1.' },
|
||||
endLine: { type: 'string', description: 'Line to stop reading from (inclusive). Default is "null", treated as Infinity.' },
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
|
@ -71,7 +80,7 @@ export const voidTools = {
|
|||
|
||||
get_dir_structure: {
|
||||
name: 'get_dir_structure',
|
||||
description: `Returns a tree diagram of all the files and folders in the URI. If results are large, the given string will be truncated (this will be indicated). If truncated, you should use this tool on a more specific folder, or just use ls_dir which supports pagination but is not recursive.`,
|
||||
description: `Returns a tree diagram of all the files and folders in the given folder URI. Call this to learn more about a folder. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
|
|
@ -79,18 +88,20 @@ export const voidTools = {
|
|||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...searchParams,
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
||||
search_files: {
|
||||
name: 'search_files',
|
||||
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`view_file_contents\` tool to view the full file contents of results. ${paginationHelper.desc}`,
|
||||
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...searchParams,
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
|
@ -110,7 +121,7 @@ export const voidTools = {
|
|||
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
|
||||
params: {
|
||||
...uriParam('file or folder'),
|
||||
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
|
||||
params: { type: 'string', description: 'Return -r here to delete recursively (if applicable). Default is the empty string.' }
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -40,12 +40,13 @@ const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder',
|
|||
export type ToolNameWithApproval = typeof toolNamesWithApproval[number]
|
||||
export const toolNamesThatRequireApproval = new Set<ToolName>(toolNamesWithApproval)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
export type ToolCallParams = {
|
||||
'view_file_contents': { uri: URI, pageNumber: number },
|
||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { rootURI: URI, pageNumber: number },
|
||||
'get_dir_structure': { rootURI: URI },
|
||||
'search_pathnames_only': { queryStr: string, pageNumber: number },
|
||||
'search_files': { queryStr: string, pageNumber: number },
|
||||
'search_pathnames_only': { queryStr: string, include: string | null, pageNumber: number },
|
||||
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
// ---
|
||||
'edit_file': { uri: URI, changeDescription: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
|
|
@ -54,8 +55,9 @@ export type ToolCallParams = {
|
|||
}
|
||||
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
'view_file_contents': { fileContents: string, hasNextPage: boolean },
|
||||
'read_file': { fileContents: string, hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_structure': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
|
|
|
|||
Loading…
Reference in a new issue