Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

custom recipes: move behind experimental flag #348

Merged
merged 10 commits into from
Jul 22, 2023
28 changes: 15 additions & 13 deletions .vscode/cody.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
"prompt": "Correct all typos and non-standard usage for the selected code."
},
"Refactor Code": {
"prompt": "Please analyze the code and suggest constructive edits that follow best practices and improve code quality and readability. Focus your responses on being clear, thoughtful, coherent and easy for me to understand. Do not make changes that alter intended functionality without explaining why. Avoid responses that are overly complex or difficult to implement. Strive to provide helpful recommendations based on other shared code snippests."
"prompt": "Please analyze the code and suggest constructive edits that follow best practices and improve code quality and readability. Focus your responses on being clear, thoughtful, coherent and easy for me to understand. Do not make changes that alter intended functionality without explaining why. Avoid responses that are overly complex or difficult to implement. Strive to provide helpful recommendations based on other shared code snippests.",
"context": {
"currentFile": true
}
},
"Generate README.md for Current Directory": {
"prompt": "Write a detailed README.md introduction for this project. If possible, briefly explain what the directory is and its key features. Use Markdown formatting. Focus on clarity and being beginner-friendly. Surround the answer with a code block to indicate that it is code.",
Expand All @@ -16,36 +19,35 @@
},
"Commit Message for Current Changes": {
"prompt": "Suggest a commit message based on current diff changes.",
"command": "git",
"args": ["diff"],
"context": {
"excludeSelection": true
"excludeSelection": true,
"command": "git diff"
},
"note": "You must have git installed and authenticated to use this recipe"
"info": "You must have git installed and authenticated to use this recipe"
},
"Debug last error from Cody app": {
"prompt": "Tell me about the most recent error in log and how I can resolve it.",
"command": "cat",
"args": ["~/Library/Logs/com.sourcegraph.cody/Cody.log"],
"context": {
"excludeSelection": true
"excludeSelection": true,
"command": "cat ~/Library/Logs/com.sourcegraph.cody/Cody.log"
},
"note": "You must have Cody app installed to use this recipe"
"info": "You must have Cody app installed to use this recipe"
},
"Generate Multiple Unit Tests": {
"prompt": "Generate at least 3 unit tests for the selected code. Provide me with full, workable unit tests. You may import common libraries like the language's built-in test framework. If there are existing test files in the directory, try to follow similar patterns and imports used in those files. If no test files exist yet, use common best practices for unit testing this language.",
"prompt": "Generate 2 or more unit tests for the selected code. Provide me with full, workable unit tests. You may import common libraries like the language's built-in test framework. If there are existing test files in the directory, try to follow similar patterns and imports used in those files. If there are no test files, use common best practices for unit testing this language.",
"context": {
"currentDir": true
"currentDir": true,
"currentFile": true
},
"note": "Works best if there are other test files in the current directory"
"info": "Works best if there are other test files in the current directory"
},
"Compare Files in Opened Tabs": {
"prompt": "Compare the code from opened tabs and explain their relationships.",
"context": {
"openTabs": true,
"excludeSelection": true
},
"description": "This recipe lets Cody analyze code from open tabs to provide insights on how they relate to each other."
"info": "This recipe lets Cody analyze code from open tabs to provide insights on how they relate to each other."
}
}
}
2 changes: 1 addition & 1 deletion lib/shared/src/chat/recipes/inline-touch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class InlineTouch implements Recipe {
const contextMessages: ContextMessage[] = []
// Add selected text and current file as context and create context messages from current directory
const selectedContext = ChatQuestion.getEditorSelectionContext(selection)
const currentDirContext = await MyPrompt.getEditorDirContext(currentDir, true)
const currentDirContext = await MyPrompt.getEditorDirContext(currentDir, selection.fileName, true)
contextMessages.push(...selectedContext, ...currentDirContext)
// Create context messages from open tabs
if (contextMessages.length < 10) {
Expand Down
147 changes: 118 additions & 29 deletions lib/shared/src/chat/recipes/my-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { truncateText } from '../../prompt/truncation'
import { Interaction } from '../transcript/interaction'

import { ChatQuestion } from './chat-question'
import { numResults } from './helpers'
import { getFileExtension, numResults } from './helpers'
import { InlineTouch } from './inline-touch'
import { Recipe, RecipeContext, RecipeID } from './recipe'

Expand All @@ -29,6 +29,7 @@ export interface CodyPromptContext {
currentDir?: boolean
currentFile?: boolean
excludeSelection?: boolean
command?: string
filePath?: string
directoryPath?: string
none?: boolean
Expand Down Expand Up @@ -58,7 +59,7 @@ export class MyPrompt implements Recipe {
await vscode.window.showErrorMessage('Please enter a valid prompt for the recipe.')
return null
}
const commandOutput = context.editor.controllers?.prompt.get()
const commandOutput = await context.editor.controllers?.prompt.get('output')
const note = ' Refer to the command output and shared code snippets to answer my quesiton.'
const truncatedText = truncateText(promptText + note, MAX_HUMAN_INPUT_TOKENS)
// Add selection file name as display when available
Expand Down Expand Up @@ -93,7 +94,7 @@ export class MyPrompt implements Recipe {
commandOutput?: string | null
): Promise<ContextMessage[]> {
const contextMessages: ContextMessage[] = []
const contextConfig = editor.controllers?.prompt.get('context')
const contextConfig = await editor.controllers?.prompt.get('context')
const isContextRequired = contextConfig
? (JSON.parse(contextConfig) as CodyPromptContext)
: defaultCodyPromptContext
Expand All @@ -116,11 +117,16 @@ export class MyPrompt implements Recipe {
// Select test files from the directory only if the prompt text includes 'test'
const isTestRequest = text.includes('test')
const currentDirContext = await MyPrompt.getCurrentDirContext(isTestRequest)
// Add package.json context if it's available for test requests
if (isTestRequest) {
const packageJSONContextMessage = await MyPrompt.getPackageJsonContext(selection?.fileName)
currentDirContext.push(...packageJSONContextMessage)
}
contextMessages.push(...currentDirContext)
}
// Create context messages from a fsPath of a workspace directory
if (isContextRequired.directoryPath?.length) {
const fileContext = await MyPrompt.getEditorDirContext(isContextRequired.directoryPath)
const fileContext = await MyPrompt.getEditorDirContext(isContextRequired.directoryPath, selection?.fileName)
contextMessages.push(...fileContext)
}
// Create context messages from a fsPath of a file
Expand All @@ -137,7 +143,7 @@ export class MyPrompt implements Recipe {
contextMessages.push(...ChatQuestion.getEditorSelectionContext(selection))
}
// Create context messages from terminal output if any
if (commandOutput) {
if (isContextRequired.command?.length && commandOutput) {
contextMessages.push(...MyPrompt.getTerminalOutputContext(commandOutput))
}
// Return the last n context messages in case there are too many
Expand Down Expand Up @@ -166,7 +172,7 @@ export class MyPrompt implements Recipe {
const fileName = vscode.workspace.asRelativePath(doc.uri.fsPath)
const truncatedContent = truncateText(fileContent.getText(), MAX_CURRENT_FILE_TOKENS)
const docAsMessage = getContextMessageWithResponse(
populateCurrentEditorContextTemplate(truncatedContent, fileName),
populateCurrentEditorContextTemplate(toJSON(truncatedContent), fileName),
{ fileName }
)
contextMessages.push(...docAsMessage)
Expand All @@ -190,7 +196,7 @@ export class MyPrompt implements Recipe {
try {
const content = await vscode.workspace.fs.readFile(fileUri)
const truncatedContent = truncateText(content.toString(), MAX_CURRENT_FILE_TOKENS)
return getContextMessageWithResponse(populateCodeContextTemplate(truncatedContent, fileName), {
return getContextMessageWithResponse(populateCodeContextTemplate(toJSON(truncatedContent), fileName), {
fileName,
})
} catch (error) {
Expand All @@ -202,38 +208,98 @@ export class MyPrompt implements Recipe {
// Create Context from files within a directory
public static async getCurrentDirContext(isTestRequest: boolean): Promise<ContextMessage[]> {
// Get current document file path
const currentDirPath = vscode.window.activeTextEditor?.document?.fileName.replace(/\/[^/]+$/, '')
if (!currentDirPath) {
const currentFileName = vscode.window.activeTextEditor?.document?.fileName
if (!currentFileName) {
return []
}
return MyPrompt.getEditorDirContext(currentDirPath, isTestRequest)
const currentDirPath = getCurrentDirPath(currentFileName)
return MyPrompt.getEditorDirContext(currentDirPath, currentFileName, isTestRequest)
}

// Create Context from Current Directory of the Active Document
// Return tests files only if testOnly is true
public static async getEditorDirContext(dirPath: string, testOnly?: boolean): Promise<ContextMessage[]> {
// get a list of files from the current directory path
const dirUri = vscode.Uri.file(dirPath)
// Get the list of files in the current directory then filter out:
// directories, non-test files, and dot files
// then returns the first 10 results
if (testOnly) {
const filesInDir = (await vscode.workspace.fs.readDirectory(dirUri)).filter(
file => file[1] === 1 && !file[0].startsWith('.') && (testOnly ? file[0].includes('test') : true)
)
// If there are no test files in the directory, use first 10 files instead
if (filesInDir.length > 0) {
return populateVscodeDirContextMessage(dirUri, filesInDir.slice(0, 10))
public static async getEditorDirContext(
dirPath: string,
currentFileName?: string,
testOnly?: boolean
): Promise<ContextMessage[]> {
try {
// get a list of files from the current directory path
const dirUri = vscode.Uri.file(dirPath)
// Get the list of files in the current directory then filter out:
// directories, non-test files, and dot files
// then returns the first 10 results
if (testOnly) {
const filesInDir = (await vscode.workspace.fs.readDirectory(dirUri)).filter(
file => file[1] === 1 && !file[0].startsWith('.') && (testOnly ? file[0].includes('test') : true)
)
// If there are no test files in the directory, use first 10 files instead
if (filesInDir.length > 0) {
return await populateVscodeDirContextMessage(dirUri, filesInDir.slice(0, 10))
}
const parentDirName = getParentDirName(dirPath)
const fileExt = currentFileName ? getFileExtension(currentFileName) : '*'
// Search for test files from the parent directory
const testFile = await vscode.workspace.findFiles(
`**/${parentDirName}/**/*test.${fileExt}}`,
'**/node_modules/**',
5
)
if (testFile.length) {
return await MyPrompt.getFilePathContext(testFile[0].fsPath)
}
}
// Get first 10 files in the directory
const filesInDir = await getFirstNFilesFromDir(dirUri, 10)
// When there is no test files, Try to add the package.json context if it's available
return await populateVscodeDirContextMessage(dirUri, filesInDir)
} catch {
return []
}
}

// Get context from the last package.json in the current file path
public static async getPackageJsonContext(filePath?: string): Promise<ContextMessage[]> {
const currentFilePath = filePath || vscode.window.activeTextEditor?.document.uri.fsPath
if (!currentFilePath) {
return []
}
// Search for the package.json from the root of the repository
const packageJsonPath = await vscode.workspace.findFiles('**/package.json', '**/node_modules/**', 1)
if (!packageJsonPath.length) {
return []
}
try {
const packageJsonUri = packageJsonPath[0]
const packageJsonContent = await vscode.workspace.fs.readFile(packageJsonUri)
// Turn the content into a json and get the scripts object only
const packageJson = JSON.parse(packageJsonContent.toString()) as Record<string, unknown>
const scripts = packageJson.scripts
const devDependencies = packageJson.devDependencies
// stringify the scripts object with devDependencies
const context = JSON.stringify({ scripts, devDependencies })
const truncatedContent = truncateText(
context.toString() || packageJsonContent.toString(),
MAX_CURRENT_FILE_TOKENS
)
const fileName = vscode.workspace.asRelativePath(packageJsonUri.fsPath)
return getContextMessageWithResponse(populateCodeContextTemplate(toJSON(truncatedContent), fileName), {
fileName,
})
} catch {
return []
}
// Get first 10 files in the directory
const filesInDir = (await vscode.workspace.fs.readDirectory(dirUri))
.filter(file => file[1] === 1 && !file[0].startsWith('.'))
.slice(0, 10)
return populateVscodeDirContextMessage(dirUri, filesInDir)
}
}

/**
* Populates context messages for files in a VS Code directory.
*
* @param dirUri - The VS Code Uri of the directory to get files from.
* @param filesInDir - An array of file name and file type tuples for the files in the director
*
*/

async function populateVscodeDirContextMessage(
dirUri: vscode.Uri,
filesInDir: [string, vscode.FileType][]
Expand All @@ -253,7 +319,7 @@ async function populateVscodeDirContextMessage(
const fileContent = await vscode.workspace.openTextDocument(fileUri)
const truncatedContent = truncateText(fileContent.getText(), MAX_CURRENT_FILE_TOKENS)
const contextMessage = getContextMessageWithResponse(
populateCurrentEditorContextTemplate(truncatedContent, fileName),
populateCurrentEditorContextTemplate(toJSON(truncatedContent), fileName),
{ fileName }
)
contextMessages.push(...contextMessage)
Expand All @@ -263,3 +329,26 @@ async function populateVscodeDirContextMessage(
}
return contextMessages
}

// Clean up the string to be used as value in JSON format
// Escape double quotes and backslashes and forward slashes
function toJSON(context: string): string {
const escaped = context.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\//g, '\\/')
return JSON.stringify(escaped)
}

// Split the directory path into parts and remove the last part to get the parent directory path
const getParentDirName = (dirPath: string): string => {
const pathParts = dirPath.split('/')
pathParts.pop()
return pathParts.pop() || ''
}

// Get the current directory path from the file path
const getCurrentDirPath = (filePath: string): string => filePath?.replace(/\/[^/]+$/, '')

// Get the first n files from a directory Uri
const getFirstNFilesFromDir = async (dirUri: vscode.Uri, n: number): Promise<[string, vscode.FileType][]> =>
(await vscode.workspace.fs.readDirectory(dirUri))
.filter(file => file[1] === 1 && !file[0].startsWith('.'))
.slice(0, n)
1 change: 1 addition & 0 deletions lib/shared/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface Configuration {
autocomplete: boolean
experimentalChatPredictions: boolean
inlineChat: boolean
experimentalCustomRecipes: boolean
experimentalGuardrails: boolean
experimentalNonStop: boolean
autocompleteAdvancedProvider: 'anthropic' | 'unstable-codegen' | 'unstable-huggingface'
Expand Down
3 changes: 1 addition & 2 deletions lib/shared/src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,7 @@ interface VsCodeFixupController {
}

interface VsCodeMyPromptController {
get(type?: string): string | null
run(command: string): string | null
get(type?: string): Promise<string | null>
menu(): Promise<void>
}

Expand Down
3 changes: 3 additions & 0 deletions vscode/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Starting from `0.2.0`, Cody is using `major.EVEN_NUMBER.patch` for release versi

### Added

- [Internal] `Custom Recipes`: An experimental feature now available behind the `cody.experimental.customRecipes` feature flag for internal testing purpose. [pull/348](https://github.com/sourcegraph/cody/pull/348)

### Fixed

### Changed
Expand All @@ -18,6 +20,7 @@ Starting from `0.2.0`, Cody is using `major.EVEN_NUMBER.patch` for release versi

- A new experimental user setting `cody.autocomplete.experimental.triggerMoreEagerly` causes autocomplete to trigger earlier, before you type a space or other non-word character.
- [Internal Only] `Custom Recipe`: Support context type selection when creating a new recipe via UI. [pull/279](https://github.com/sourcegraph/cody/pull/279)
- New `/open` command for opening workspace files from chat box. [pull/327](https://github.com/sourcegraph/cody/pull/327)

### Fixed

Expand Down
Loading
Loading