This commit is contained in:
Andrew Pareles 2025-02-15 00:02:17 -08:00
parent 05d8b3a982
commit bc6150aeac
11 changed files with 97 additions and 61 deletions

View file

@ -42,6 +42,7 @@ 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, }
@ -65,7 +66,6 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => {
const model = editor.getModel();
@ -1138,6 +1138,44 @@ 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) {
if (opts.type === 'rewrite') {

View file

@ -22,7 +22,7 @@ export const errorDetails = (fullError: Error | null): string | null => {
}
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string, tools: { name: string, args: string }[] }) => void
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }

View file

@ -29,7 +29,7 @@ const paginationHelper = {
param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, }
} as const
export const contextTools = {
export const voidTools: { [name: string]: InternalToolInfo } = {
read_file: {
name: 'read_file',
description: 'Returns file contents of a given URI.',
@ -73,12 +73,11 @@ export const contextTools = {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
}
export type ContextToolName = keyof typeof contextTools
type ContextToolParamNames<T extends ContextToolName> = keyof typeof contextTools[T]['params']
type ContextToolParams<T extends ContextToolName> = { [paramName in ContextToolParamNames<T>]: unknown }
export type ToolName = keyof typeof voidTools
type ToolParamNames<T extends ToolName> = keyof typeof voidTools[T]['params']
type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: unknown }
@ -121,7 +120,7 @@ export class ToolService implements IToolService {
readonly _serviceBrand: undefined;
public contextToolCallFns
public toolFns
constructor(
@IFileService fileService: IFileService,
@ -133,32 +132,32 @@ export class ToolService implements IToolService {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.contextToolCallFns = {
read_file: async ({ uri: uriStr }: ContextToolParams<'read_file'>) => {
this.toolFns = {
read_file: async ({ uri: uriStr }: ToolParamsObj<'read_file'>) => {
const uri = validateURI(uriStr)
const fileContents = await VSReadFileRaw(fileService, uri)
return fileContents ?? '(could not read file)'
},
list_dir: async ({ uri: uriStr }: ContextToolParams<'list_dir'>) => {
list_dir: async ({ uri: uriStr }: ToolParamsObj<'list_dir'>) => {
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 }: ContextToolParams<'pathname_search'>) => {
pathname_search: async ({ query: queryStr }: ToolParamsObj<'pathname_search'>) => {
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 query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
const data = await searchService.fileSearch(query, CancellationToken.None);
const data = await searchService.fileSearch(query, CancellationToken.None)
const URIs = data.results.map(({ resource, results }) => resource.fsPath)
return URIs
},
search: async ({ query: queryStr }: ContextToolParams<'search'>) => {
search: async ({ query: queryStr }: ToolParamsObj<'search'>) => {
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 query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
const data = await searchService.textSearch(query, CancellationToken.None);
const data = await searchService.textSearch(query, CancellationToken.None)
const URIs = data.results.map(({ resource, results }) => resource)
return URIs
},

View file

@ -6,7 +6,7 @@
import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
import { InternalToolInfo } from '../../common/toolsService.js';
import { InternalToolInfo, voidTools } from '../../common/toolsService.js';
@ -45,7 +45,7 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
messages: messages,
model: modelName,
max_tokens: maxTokens,
// tools: [toAnthropicTool(contextTools.list_dir)]
tools: [toAnthropicTool(voidTools.list_dir)]
});
@ -55,33 +55,31 @@ export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages,
})
// can do tool use streaming
const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
stream.on('streamEvent', e => {
if (e.type === 'content_block_start') {
if (e.content_block.type !== 'tool_use') return
const index = e.index
if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
toolCallOfIndex[index].name += e.content_block.name ?? ''
toolCallOfIndex[index].args += e.content_block.input ?? ''
}
else if (e.type === 'content_block_delta') {
if (e.delta.type !== 'input_json_delta') return
toolCallOfIndex[e.index].args += e.delta.partial_json
}
// TODO!!!!!
// onText({})
})
// // can do tool use streaming
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
// stream.on('streamEvent', e => {
// if (e.type === 'content_block_start') {
// if (e.content_block.type !== 'tool_use') return
// const index = e.index
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
// toolCallOfIndex[index].name += e.content_block.name ?? ''
// toolCallOfIndex[index].args += e.content_block.input ?? ''
// }
// else if (e.type === 'content_block_delta') {
// if (e.delta.type !== 'input_json_delta') return
// toolCallOfIndex[e.index].args += e.delta.partial_json
// }
// // TODO!!!!!
// // onText({})
// })
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (response) => {
// stringify the response's content
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n')
const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, input: c.input } : null).filter(c => !!c)
const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n')
const tools = response.content.map(c => c.type === 'tool_use' ? { name: c.name, args: JSON.stringify(c.input) } : null).filter(c => !!c)
console.log("TOOLS!!!!", typeof tools[0]?.input, JSON.stringify(tools, null, 2))
onFinalMessage({ fullText: content, })
onFinalMessage({ fullText: content, tools })
})
stream.on('error', (error) => {

View file

@ -35,7 +35,7 @@ export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messag
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
onFinalMessage({ fullText, tools: [] });
})
.catch((error) => {
onError({ message: error + '', fullError: error })

View file

@ -32,7 +32,7 @@ export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages
onText({ newText, fullText });
}
onFinalMessage({ fullText });
onFinalMessage({ fullText, tools: [] });
})
.catch(error => {
onError({ message: error + '', fullError: error });

View file

@ -36,7 +36,7 @@ export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messa
onText({ newText, fullText });
}
onFinalMessage({ fullText });
onFinalMessage({ fullText, tools: [] });
})
.catch(error => {
onError({ message: error + '', fullError: error });

View file

@ -68,7 +68,7 @@ export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
onFinalMessage({ fullText, tools: [] });
})
// when error/fail
.catch((error) => {
@ -100,14 +100,13 @@ export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, on
for await (const chunk of stream) {
const newText = chunk.message.content;
// chunk.message.tool_calls[0].function.arguments
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
onFinalMessage({ fullText, tools: [] });
})
// when error/fail

View file

@ -111,30 +111,32 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe
// openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models
onFinalMessage({ fullText: 'TODO' })
}
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools }) => {
let fullText = ''
const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider })
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName,
messages: messages,
stream: true,
// tools: Object.keys(contextTools).map(name => toOpenAITool(name, contextTools[name as ContextToolName])),
tools: tools?.map(tool => toOpenAITool(tool)),
}
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
@ -153,7 +155,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, on
onText({ newText, fullText });
}
onFinalMessage({ fullText });
onFinalMessage({ fullText, tools: Object.keys(toolCallOfIndex).map(index => toolCallOfIndex[index]) });
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {

View file

@ -8,7 +8,7 @@ import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicChat } from './anthropic.js';
import { sendOllamaFIM, sendOllamaChat } from './ollama.js';
import { sendOpenAIChat, sendOpenAIFIM } from './openai.js';
import { sendOpenAIChat } from './openai.js';
import { sendGeminiChat } from './gemini.js';
import { sendGroqChat } from './groq.js';
import { sendMistralChat } from './mistral.js';
@ -107,10 +107,10 @@ export const sendLLMMessage = ({
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
const onFinalMessage: OnFinalMessage = ({ fullText, tools }) => {
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText })
onFinalMessage_({ fullText, tools })
}
const onError: OnError = ({ message: error, fullError }) => {
@ -141,7 +141,7 @@ export const sendLLMMessage = ({
case 'openRouter':
case 'deepseek':
case 'openAICompatible':
if (messagesType === 'FIMMessage') sendOpenAIFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI FIM', tools: [] })
else /* */ sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
break;
case 'ollama':
@ -149,19 +149,19 @@ export const sendLLMMessage = ({
else /* */ sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
break;
case 'anthropic':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' })
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', tools: [] })
else /* */ sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
break;
case 'gemini':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM' })
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Gemini FIM', tools: [] })
else /* */ sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
break;
case 'groq':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM' })
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Groq FIM', tools: [] })
else /* */ sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
break;
case 'mistral':
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM' })
if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Mistral FIM', tools: [] })
else /* */ sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, tools });
break;
default:

View file

@ -100,7 +100,7 @@ export class LLMMessageChannel implements IServerChannel {
const mainThreadParams: SendLLMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
onFinalMessage: ({ fullText, tools }) => { this._onFinalMessage_llm.fire({ requestId, fullText, tools }); },
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
abortRef: this._abortRefOfRequestId_llm[requestId],
}