From ac1788ae9a7fbf034ec58d0a7f5dffba6be444ff Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Feb 2025 21:25:23 -0800 Subject: [PATCH] tool pages work, improve prompt --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/prompt/prompts.ts | 24 +++++--- .../react/src/markdown/ChatMarkdownRender.tsx | 4 +- .../contrib/void/common/toolsService.ts | 61 ++++++++++++++----- .../llmMessage/postprocessToolCalls.ts | 2 +- 5 files changed, 64 insertions(+), 29 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7f53a043..fd7e4288 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -356,7 +356,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // add user's message to chat history const instructions = userMessage - const userMessageContent = await chat_userMessageContent(instructions, currSelns, currSelns) + const userMessageContent = await chat_userMessageContent(instructions, currSelns) const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._modelService, this._fileService) const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 1e9dd2a9..f04fcb5c 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -33,10 +33,12 @@ For example, if the user asks you to "make this file look nicer", make sure your You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. If you are given tools: +- Only use tools if the user asks you to do something. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools. - You are allowed to use tools without asking for permission. - Feel free to use tools to gather context, make suggestions, etc. - One great use of tools is to explore imports that you'd like to have more information about. -- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. +- Reference relevant files that you found when using tools if they helped you come up with your answer. +- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not even refer to "pages" of results, just say you're getting more results. Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. @@ -176,14 +178,14 @@ const stringifyFileSelections = async (fileSelections: FileSelection[], modelSer return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') } const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { - return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') + return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') || null } const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => { if (!currSelns) return '' return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n') } -export const chat_userMessageContent = async (instructions: string, prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null) => { +export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => { const selnsStr = stringifySelectionNames(currSelns) @@ -198,6 +200,8 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | // ADD IN FILES AT TOP const allSelections = [...currSelns || [], ...prevSelns || []] + if (allSelections.length === 0) return null + const codeSelections: CodeSelection[] = [] const fileSelections: FileSelection[] = [] const filesURIs = new Set() @@ -219,17 +223,17 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService) const selnsStr = stringifyCodeSelections(codeSelections) - let str = '' - str += 'ALL FILE CONTENTS\n' - if (filesStr) str += `${filesStr}\n` - if (selnsStr) str += `${selnsStr}\n` + if (filesStr || selnsStr) return `\ +ALL FILE CONTENTS +${filesStr} +${selnsStr}` - return str; + return null } -export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | undefined) => { - if (userMessage) return `${userMessage}\n${selectionsString}\n` +export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | null) => { + if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}` else return userMessage } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 5de08900..3e77a6df 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -29,7 +29,7 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => -const ApplyButtonsOnHover = ({ applyStr, applyBoxId }: { applyStr: string, applyBoxId: string }) => { +const ApplyButtonsOnHover = ({ applyStr }: { applyStr: string }) => { const accessor = useAccessor() const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) @@ -120,7 +120,7 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati return } + buttonsOnHover={applyBoxId && } /> } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 7620b2cd..4a0933a2 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -1,7 +1,7 @@ import { CancellationToken } from '../../../../base/common/cancellation.js' import { URI } from '../../../../base/common/uri.js' import { IModelService } from '../../../../editor/common/services/model.js' -import { IFileService, IFileStat } from '../../../../platform/files/common/files.js' +import { IFileService } from '../../../../platform/files/common/files.js' import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js' @@ -93,22 +93,46 @@ export type ToolCallReturnType export type ToolFns = { [T in ToolName]: (p: string) => Promise> } export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType) => string } +const MAX_DEPTH = 1 +const MAX_CHILDREN = 500 +async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise { + let output = ''; -async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise { - let output = '' - function traverseChildren(children: IFileStat[], depth: number) { - const indentation = ' '.repeat(depth); - for (const child of children) { - output += `${indentation}- ${child.name}\n`; - traverseChildren(child.children ?? [], depth + 1); + const indentation = (depth: number, isLast: boolean): string => { + if (depth === 0) return ''; + return `${'| '.repeat(depth - 1)}${isLast ? '└── ' : '├── '}`; + }; + + async function traverseChildren(uri: URI, depth: number, isLast: boolean) { + const stat = await fileService.resolve(uri, { resolveMetadata: false }); + + if ((depth === 0 && pageNumber === 1) || depth !== 0) + output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; // TODO say where symlink links to + + // list children + const originalChildrenLength = stat.children?.length ?? 0 + const fromChildIdx = MAX_CHILDREN * (pageNumber - 1) + const toChildIdx = MAX_CHILDREN * pageNumber - 1 // INCLUSIVE + const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; + + if (!stat.isDirectory) return; + + if (listChildren.length === 0) return + if (depth === MAX_DEPTH) return // right now MAX_DEPTH=1 to make pagination work nicely + + for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN); i++) { + await traverseChildren(listChildren[i].resource, depth + 1, i === listChildren.length - 1); } + const nCutoffChildren = (originalChildrenLength - 1) - toChildIdx + if (nCutoffChildren > 0) { + output += `${indentation(depth + 1, true)}(${nCutoffChildren} results remaining...)\n` + } + } - const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); - // kickstart recursion - output += `${stat.name}\n`; - traverseChildren(stat.children ?? [], 1); + await traverseChildren(rootURI, 0, false); + console.log('OUTPUT', output); return output; } @@ -119,6 +143,12 @@ const validateURI = (uriStr: unknown) => { return uri } +const validatePageNum = (pageNumberUnknown: unknown) => { + const proposedPageNum = Number.parseInt(pageNumberUnknown + '') + const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1 + const pageNumber = num < 1 ? 1 : num + return pageNumber +} export interface IToolsService { readonly _serviceBrand: undefined; toolFns: ToolFns; @@ -170,12 +200,13 @@ export class ToolsService implements IToolsService { list_dir: async (s: string) => { const o = parseObj(s) if (!o) return invalidToolParamMsg - const { uri: uriStr } = o + const { uri: uriStr, pageNumber: pageNumberUnknown } = o const uri = validateURI(uriStr) + const pageNumber = validatePageNum(pageNumberUnknown) + // TODO!!!! check to make sure in workspace - // TODO check to make sure is not gitignored - const treeStr = await generateDirectoryTreeMd(fileService, uri) + const treeStr = await generateDirectoryTreeMd(fileService, uri, pageNumber) return treeStr }, pathname_search: async (s: string) => { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts index e9bb2b7c..2feeeb80 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts @@ -1,4 +1,4 @@ -import { ToolName, toolNames } from '../../common/toolsService'; +import { ToolName, toolNames } from '../../common/toolsService.js';