context tools

This commit is contained in:
Andrew Pareles 2025-02-08 22:53:13 -08:00
parent 18b278577d
commit 00da05f24f
5 changed files with 240 additions and 115 deletions

View file

@ -0,0 +1,193 @@
import { URI } from '../../../base/common/uri'
import { IModelService } from '../../../editor/common/services/model'
import { VSReadFile } from '../../../workbench/contrib/void/browser/helpers/readFile'
import { IFileService, IFileStat } from '../../files/common/files'
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions'
import { createDecorator } from '../../instantiation/common/instantiation'
const pagination = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (optional, defaults to 1).' }, }
} as const
// we do this using Anthropic's style and convert to OpenAI style later
export type InternalToolInfo = {
description: string,
params: {
[paramName: string]: { type: string, description: string | undefined } // name -> type
},
required: string[], // required paramNames
}
const contextTools = {
read_file: {
description: 'Returns file contents of a given URI.',
params: {
uri: { type: 'string', description: undefined },
},
required: ['uri'],
},
list_dir: {
description: `Returns all file names and folder names in a given URI. ${pagination.desc}`,
params: {
uri: { type: 'string', description: undefined },
...pagination.param
},
required: ['uri'],
},
pathname_search: {
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${pagination.desc}`,
params: {
query: { type: 'string', description: undefined },
...pagination.param,
},
required: ['query']
},
grep_search: {
description: `Returns all code excerpts containing the given string or grep query. Does not search filename. As a follow-up, you may want to use read_file to view the full file contents of the results. ${pagination.desc}`,
params: {
query: { type: 'string', description: undefined },
...pagination.param,
},
required: ['query'],
},
// semantic_search: {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
} as const satisfies { [name: string]: InternalToolInfo }
type ContextToolName = keyof typeof contextTools
type ContextParamNames<T extends ContextToolName> = keyof typeof contextTools[T]['params']
type ContextParams<T extends ContextToolName> = { [paramName in ContextParamNames<T>]: string }
type ContextToolCallFns = {
[ToolName in ContextToolName]: ((p: (ContextParams<ToolName>)) => Promise<string>)
}
/*
Generates something that looks like this:
+ folder1
file1.py
subfolder1
file1.json
file2.py
another_file.txt
folder2
script.js
styles.css
*/
/**
* Generates a Markdown tree starting at the given URI.
* The root folder is printed as a header (without a bullet).
*/
export async function generateMarkdownTree(fileService: IFileService, uri: URI): Promise<string> {
let output = ''
function traverseChildren(children: IFileStat[], depth: number) {
const indentation = ' '.repeat(depth);
for (const child of children) {
output += `${indentation}- ${child.name}\n`;
output += traverseChildren(child.children ?? [], depth + 1);
}
}
const stat = await fileService.resolve(uri, { resolveMetadata: false });
// kickstart recursion
output += `${stat.name}\n`;
traverseChildren(stat.children ?? [], 1);
return output;
}
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('(uri was not a string)')
console.log('uriStr!!!!', uriStr)
const uri = URI.file(uriStr)
console.log('uri!!!!', uri)
return uri
}
export interface IToolService {
readonly _serviceBrand: undefined;
callContextTool: <T extends ContextToolName>(toolName: T, params: ContextParams<T>) => Promise<string>
}
export const IToolService = createDecorator<IToolService>('ToolService');
// implemented by calling channel
export class ToolService implements IToolService {
readonly _serviceBrand: undefined;
contextToolCallFns: ContextToolCallFns
constructor(
@IFileService fileService: IFileService,
@IModelService modelService: IModelService,
) {
this.contextToolCallFns = {
read_file: async ({ uri: uriStr }) => {
const uri = validateURI(uriStr)
const fileContents = await VSReadFile(modelService, uri)
return fileContents ?? '(could not read file)'
},
list_dir: async ({ uri: uriStr }) => {
const uri = validateURI(uriStr)
const treeStr = await generateMarkdownTree(fileService, uri)
return treeStr
},
pathname_search: async ({ query }) => {
return ''
},
grep_search: async ({ query }) => {
return ''
},
}
}
callContextTool: IToolService['callContextTool'] = (toolName, params) => {
return this.contextToolCallFns[toolName](params)
}
}
registerSingleton(IToolService, ToolService, InstantiationType.Eager);

View file

@ -6,6 +6,27 @@
import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
import { InternalToolInfo } from '../../common/toolsService.js';
export const toAnthropicTool = (toolName: string, toolInfo: InternalToolInfo) => {
const { description, params, required } = toolInfo
return {
name: toolName,
description: description,
input_schema: {
type: 'object',
properties: params,
required: required,
}
} satisfies Anthropic.Messages.Tool
}
export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {

View file

@ -6,11 +6,33 @@
import OpenAI from 'openai';
import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js';
import { Model } from 'openai/resources/models.js';
import { InternalToolInfo } from '../../common/toolsService.js';
// import { parseMaxTokensStr } from './util.js';
// https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
// https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting
export const toOpenAITool = (toolName: string, toolInfo: InternalToolInfo) => {
const { description, params, required } = toolInfo
return {
type: 'function',
function: {
name: toolName,
description: description,
parameters: {
type: 'object',
properties: params,
required: required,
}
}
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
}
// might not currently be used in the code

View file

@ -1,111 +0,0 @@
import Anthropic from '@anthropic-ai/sdk'
import OpenAI from 'openai'
const pagination = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (optional, defaults to 1).' }, }
} as const
// we do this using Anthropic's style and convert to OpenAI style later
type InternalToolInfo = {
description: string,
params: {
[paramName: string]: { type: string, description: string | undefined } // name -> type
},
required: string[], // required paramNames
}
const contextTools = {
read_file: {
description: 'Returns file contents of a given URI.',
params: {
uri: { type: 'string', description: undefined },
},
required: ['uri'],
},
list_dir: {
description: `Returns all file names and folder names in a given URI. ${pagination.desc}`,
params: {
uri: { type: 'string', description: undefined },
...pagination.param
},
required: ['uri'],
},
pathname_search: {
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${pagination.desc}`,
params: {
query: { type: 'string', description: undefined },
...pagination.param,
},
required: ['query']
},
grep_search: {
description: `Returns all code excerpts containing the given string or grep query. Does not search filename. As a follow-up, you may want to use read_file to view the full file contents of the results. ${pagination.desc}`,
params: {
query: { type: 'string', description: undefined },
...pagination.param,
},
required: ['query'],
},
// semantic_search: {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
} as const satisfies { [name: string]: InternalToolInfo }
type ContextToolName = keyof typeof contextTools
type ContextParams<T extends ContextToolName> = keyof typeof contextTools[T]['params']
const contextFunctions: { [ToolName in ContextToolName]: (p: ({ [paramName in ContextParams<ToolName>]: string })) => string } = {
read_file: ({ uri }) => {
return ''
},
list_dir: ({ }) => {
return ''
},
pathname_search: ({ }) => {
return ''
},
grep_search: ({ }) => {
return ''
},
}
const toOpenAITool = (toolName: string, toolInfo: InternalToolInfo) => {
const { description, params, required } = toolInfo
return {
type: 'function',
function: {
name: toolName,
description: description,
parameters: {
type: 'object',
properties: params,
required: required,
}
}
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
}
const toAnthropicTool = (toolName: string, toolInfo: InternalToolInfo) => {
const { description, params, required } = toolInfo
return {
name: toolName,
description: description,
input_schema: {
type: 'object',
properties: params,
required: required,
}
} satisfies Anthropic.Messages.Tool
}

View file

@ -133,8 +133,8 @@ Store Result: After computing fib(n), the result is stored in memo for future re
`
type FileSelnLocal = FileSelection & { content: string }
const stringifyFileSelection = ({ fileURI, selectionStr, range, content }: FileSelnLocal) => {
type FileSelnLocal = { fileURI: URI, content: string }
const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
\`\`\`${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}