diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index ff20a6ec..0c5b761a 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,6 +14,7 @@ import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessage, chat_systemMessage } from './prompt/prompts.js'; +import { IToolsService, ToolName, voidTools } from '../common/toolsService.js'; // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { @@ -60,6 +61,13 @@ export type ChatMessage = content: string; displayContent?: undefined; } + | { + role: 'tool'; + name: string; // internal use + params: string | null; // internal use + content: string | null; // summary of the tool to the LLM + displayContent: string | null; // text message of result + } // a 'thread' means a chat message history export type ChatThreads = { @@ -124,7 +132,7 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; - _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; + useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; editUserMessageAndStreamResponse(userMessage: string, messageIdx: number): Promise; addUserMessageAndStreamResponse(userMessage: string): Promise; @@ -151,6 +159,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IStorageService private readonly _storageService: IStorageService, @IModelService private readonly _modelService: IModelService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, + @IToolsService private readonly _toolsService: IToolsService, ) { super() @@ -254,14 +263,120 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- - finishStreaming = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { + private _finishStreamingTextMessage = (threadId: string, content: string, error?: { message: string, fullError: Error | null }) => { // add assistant's message to chat history, and clear selection - const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null } - this._addMessageToThread(threadId, assistantHistoryElt) + this._addMessageToThread(threadId, { role: 'assistant', content, displayContent: content || null }) this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error }) } + + + async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { + + const thread = this.getCurrentThread() + const threadId = thread.id + + let threadStaging = thread.staging + + const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing + const { selections: currSelns, } = currStaging + + // add user's message to chat history + const instructions = userMessage + const content = await chat_userMessage(instructions, currSelns, this._modelService) + const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } + this._addMessageToThread(threadId, userHistoryElt) + + this._setStreamState(threadId, { error: undefined }) + + + + // agent loop + + + let shouldContinue = false + do { + shouldContinue = false + + console.log('Q') + + let res_: () => void + const awaitable = new Promise((res, rej) => { res_ = res }) + + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: 'Ctrl+L', + logging: { loggingName: `Agent` }, + messages: [ + { role: 'system', content: chat_systemMessage }, + ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + ], + tools: [voidTools['read_file']], + + onText: ({ fullText }) => { + this._setStreamState(threadId, { messageSoFar: fullText }) + }, + onFinalMessage: async ({ fullText, tools }) => { + if (tools.length === 0) { + this._finishStreamingTextMessage(threadId, fullText) + } + else { + for (const tool of tools) { + if (!(tool.name in this._toolsService.toolFns)) { + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: `Error: This tool was not recognized, so it was not called.`, displayContent: `Error: tool not recognized.`, }) + } + else { + const toolName = tool.name as ToolName + const toolResult = await this._toolsService.toolFns[toolName](JSON.parse(tool.args)) + const string = this._toolsService.toolResultToString[toolName](toolResult as any) + this._addMessageToThread(threadId, { role: 'tool', name: tool.name, params: tool.args, content: string, displayContent: string, }) + shouldContinue = true + } + } + } + res_() + }, + onError: (error) => { + this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + res_() + }, + }) + if (llmCancelToken === null) return + this._setStreamState(threadId, { streamingToken: llmCancelToken }) + + await awaitable + } + while (shouldContinue); + + + + + // const llmCancelToken = this._llmMessageService.sendLLMMessage({ + // messagesType: 'chatMessages', + // logging: { loggingName: 'Chat' }, + // useProviderFor: 'Ctrl+L', + // messages: [ + // { role: 'system', content: chat_systemMessage }, + // ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), + // ], + // onText: ({ newText, fullText }) => { + // this._setStreamState(threadId, { messageSoFar: fullText }) + // }, + // onFinalMessage: ({ fullText: content }) => { + // this._finishStreaming(threadId, content) + // }, + // onError: (error) => { + // this._finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) + // }, + + // }) + // if (llmCancelToken === null) return + // this._setStreamState(threadId, { streamingToken: llmCancelToken }) + + } + + async editUserMessageAndStreamResponse(userMessage: string, messageIdx: number) { const thread = this.getCurrentThread() @@ -284,58 +399,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { } }, true) - // stream the edit + // re-add the message and stream it this.addUserMessageAndStreamResponse(userMessage, messageToReplace.staging) } - async addUserMessageAndStreamResponse(userMessage: string, stagingOverride?: StagingInfo | null) { - const thread = this.getCurrentThread() - const threadId = thread.id - - let threadStaging = thread.staging - - const currStaging = stagingOverride ?? threadStaging ?? defaultStaging // don't use _useFocusedStagingState to avoid race conditions with focusing - const { selections: currSelns, } = currStaging - - // add user's message to chat history - const instructions = userMessage - const content = await chat_userMessage(instructions, currSelns, this._modelService) - const userHistoryElt: ChatMessage = { role: 'user', content: content, displayContent: instructions, selections: currSelns, staging: null, } - this._addMessageToThread(threadId, userHistoryElt) - - this._setStreamState(threadId, { error: undefined }) - - const llmCancelToken = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - logging: { loggingName: 'Chat' }, - useProviderFor: 'Ctrl+L', - messages: [ - { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), - ], - onText: ({ newText, fullText }) => { - this._setStreamState(threadId, { messageSoFar: fullText }) - }, - onFinalMessage: ({ fullText: content }) => { - this.finishStreaming(threadId, content) - }, - onError: (error) => { - this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) - }, - - }) - if (llmCancelToken === null) return - this._setStreamState(threadId, { streamingToken: llmCancelToken }) - - } cancelStreaming(threadId: string) { const llmCancelToken = this.streamState[threadId]?.streamingToken if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '') + this._finishStreamingTextMessage(threadId, this.streamState[threadId]?.messageSoFar ?? '') } dismissStreamError(threadId: string): void { @@ -475,7 +550,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) - _useFocusedStagingState(messageIdx?: number | undefined) { + useFocusedStagingState(messageIdx?: number | undefined) { const defaultStaging = { isBeingEdited: false, selections: [], text: '' } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2db9cb90..52af46c2 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -42,7 +42,6 @@ import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, _InternalLLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { VSReadFile } from './helpers/readFile.js'; -import { voidTools } from '../common/toolsService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -1140,40 +1139,6 @@ class EditCodeService extends Disposable implements IEditCodeService { - async startAgent(queryStr: string) { - // agent loop - const messages: LLMChatMessage[] = [] - - while (true) { - await new Promise((res, rej) => { - this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - tools: [voidTools['read_file']], - useProviderFor: 'Apply', - logging: { loggingName: `Agent` }, - messages, - onText: ({ fullText }) => { - - }, - onFinalMessage: async ({ fullText, tools }) => { - res(tools) - }, - onError: (e) => { - }, - }) - }) - } - - - - - } - - - stopAgent() { - - } - public startApplying(opts: StartApplyingOpts) { diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts index b0f154d1..39cd310d 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -11,7 +11,11 @@ export const VSReadFile = async (modelService: IModelService, uri: URI): Promise } export const VSReadFileRaw = async (fileService: IFileService, uri: URI) => { - const res = await fileService.readFile(uri) - const str = res.value.toString() - return str + try { + const res = await fileService.readFile(uri) + const str = res.value.toString() + return str + } catch (e) { + return null + } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index c658b10b..73118cf8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -551,7 +551,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM const chatThreadsService = accessor.get('IChatThreadService') // edit mode state - const [staging, setStaging] = chatThreadsService._useFocusedStagingState(messageIdx) + const [staging, setStaging] = chatThreadsService.useFocusedStagingState(messageIdx) const mode: ChatBubbleMode = staging.isBeingEdited ? 'edit' : 'display' const [isFocused, setIsFocused] = useState(false) const [isHovered, setIsHovered] = useState(false) @@ -682,6 +682,9 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = } + else if (role === 'tool'){ + chatbubbleContents = chatMessage.name + } return
{ const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const [staging, setStaging] = chatThreadsService._useFocusedStagingState() + const [staging, setStaging] = chatThreadsService.useFocusedStagingState() // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) @@ -822,7 +825,7 @@ export const SidebarChat = () => { const prevMessagesHTML = useMemo(() => { return previousMessages.map((message, i) => - + ) }, [previousMessages]) @@ -836,6 +839,7 @@ export const SidebarChat = () => { const messagesHTML = setStaging({ ...staging, selections: s }) diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index dcfd2c67..f6ea2a2f 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -27,7 +27,7 @@ export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } export type LLMChatMessage = { - role: 'system' | 'user' | 'assistant'; + role: 'system' | 'user' | 'assistant' | 'tool'; content: string; } diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 43b18cb8..32004ff2 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -29,7 +29,7 @@ const paginationHelper = { param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } } as const -export const voidTools: { [name: string]: InternalToolInfo } = { +export const voidTools = { read_file: { name: 'read_file', description: 'Returns file contents of a given URI.', @@ -73,16 +73,22 @@ export const voidTools: { [name: string]: InternalToolInfo } = { // description: 'Searches files semantically for the given string query.', // // RAG // }, -} +} satisfies { [name: string]: InternalToolInfo } export type ToolName = keyof typeof voidTools -type ToolParamNames = keyof typeof voidTools[T]['params'] -type ToolParamsObj = { [paramName in ToolParamNames]: unknown } - - +export type ToolParamNames = keyof typeof voidTools[T]['params'] +export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } +export type ToolCallReturnType + = T extends 'read_file' ? Promise + : T extends 'list_dir' ? Promise + : T extends 'pathname_search' ? Promise + : T extends 'search' ? Promise + : never +export type ToolFns = { [T in ToolName]: (p: string) => ToolCallReturnType } +export type ToolResultToString = { [T in ToolName]: (result: Awaited>) => string } async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI): Promise { @@ -110,17 +116,21 @@ const validateURI = (uriStr: unknown) => { return uri } -export interface IToolService { +export interface IToolsService { readonly _serviceBrand: undefined; + toolFns: ToolFns; + toolResultToString: ToolResultToString; } -export const IToolService = createDecorator('ToolService'); +export const IToolsService = createDecorator('ToolsService'); -export class ToolService implements IToolService { +export class ToolsService implements IToolsService { readonly _serviceBrand: undefined; - public toolFns + public toolFns: ToolFns + public toolResultToString: ToolResultToString + constructor( @IFileService fileService: IFileService, @@ -132,29 +142,56 @@ export class ToolService implements IToolService { const queryBuilder = instantiationService.createInstance(QueryBuilder); + const parseObj = (s: string): { [s: string]: unknown } | null => { + try { + const o = JSON.parse(s) + return o + } + catch (e) { + return null + } + } + + const invalidToolParamMsg = '(LLM parameter format was invalid for this tool)' this.toolFns = { - read_file: async ({ uri: uriStr }: ToolParamsObj<'read_file'>) => { + read_file: async (s: string) => { + const o = parseObj(s) + if (!o) return invalidToolParamMsg + const { uri: uriStr } = o + const uri = validateURI(uriStr) const fileContents = await VSReadFileRaw(fileService, uri) - return fileContents ?? '(could not read file)' + return fileContents ?? invalidToolParamMsg }, - list_dir: async ({ uri: uriStr }: ToolParamsObj<'list_dir'>) => { + list_dir: async (s: string) => { + const o = parseObj(s) + if (!o) return invalidToolParamMsg + const { uri: uriStr } = o + const uri = validateURI(uriStr) // TODO!!!! check to make sure in workspace // TODO check to make sure is not gitignored const treeStr = await generateDirectoryTreeMd(fileService, uri) return treeStr }, - pathname_search: async ({ query: queryStr }: ToolParamsObj<'pathname_search'>) => { - if (typeof queryStr !== 'string') return '(Error: query was not a string)' + pathname_search: async (s: string) => { + const o = parseObj(s) + if (!o) return invalidToolParamMsg + const { query: queryStr } = o + + if (typeof queryStr !== 'string') return 'Error: query was not a string' const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, }) const data = await searchService.fileSearch(query, CancellationToken.None) - const URIs = data.results.map(({ resource, results }) => resource.fsPath) + const URIs = data.results.map(({ resource, results }) => resource) return URIs }, - search: async ({ query: queryStr }: ToolParamsObj<'search'>) => { - if (typeof queryStr !== 'string') return '(Error: query was not a string)' + search: async (s: string) => { + const o = parseObj(s) + if (!o) return '(could not search)' + const { query: queryStr } = o + + if (typeof queryStr !== 'string') return 'Error: query was not a string' const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri)) const data = await searchService.textSearch(query, CancellationToken.None) @@ -164,6 +201,23 @@ export class ToolService implements IToolService { } + this.toolResultToString = { + read_file: (URIs) => { + return URIs + }, + list_dir: (URIs) => { + return URIs + }, + pathname_search: (URIs) => { + if (typeof URIs === 'string') return URIs + return URIs.map(uri => uri.fsPath).join('\n') + }, + search: (URIs) => { + if (typeof URIs === 'string') return URIs + return URIs.map(uri => uri.fsPath).join('\n') + }, + } + } @@ -171,5 +225,5 @@ export class ToolService implements IToolService { } -registerSingleton(IToolService, ToolService, InstantiationType.Eager); +registerSingleton(IToolsService, ToolsService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts index 75ae7d89..aefe6c34 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts @@ -6,7 +6,7 @@ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; -import { InternalToolInfo, voidTools } from '../../common/toolsService.js'; +import { InternalToolInfo } from '../../common/toolsService.js'; @@ -28,7 +28,7 @@ export const toAnthropicTool = (toolInfo: InternalToolInfo) => { -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, tools }) => { const thisConfig = settingsOfProvider.anthropic @@ -45,8 +45,8 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, messages: messages, model: modelName, max_tokens: maxTokens, - tools: [toAnthropicTool(voidTools.list_dir)] - }); + tools: tools?.map(tool => toAnthropicTool(tool)) + }) // when receive text