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: add comments, debug, basic e2e test #365

Merged
merged 15 commits into from
Jul 25, 2023
11 changes: 1 addition & 10 deletions .vscode/cody.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
{
"description": "This file is used for building custom workspace recipes for Cody by Sourcegraph.",
"recipes": {
"Spell Checker": {
"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.",
"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.",
"context": {
Expand All @@ -34,7 +25,7 @@
"info": "You must have Cody app installed to use this recipe"
},
"Generate Multiple Unit Tests": {
"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.",
"prompt": "Generate 2 or more unit tests for the selected code. Provide me with full, workable unit tests. If there are existing test files in the directory, try to follow similar patterns and imports used in those files. You may import common libraries like the language's built-in test framework. If there are no test files, use common best practices for unit testing this language. Make sure to include all the imports needed to run the tests.",
"context": {
"currentDir": true,
"currentFile": true
Expand Down
72 changes: 43 additions & 29 deletions lib/shared/src/chat/recipes/my-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,8 @@ export class MyPrompt implements Recipe {
await vscode.window.showErrorMessage('Please enter a valid prompt for the recipe.')
return null
}
const commandOutput = (await context.editor.controllers?.prompt?.get('output')) ?? null
if (commandOutput === null) {
await vscode.window.showErrorMessage('No command output.')
return null
}
const note = ' Refer to the command output and shared code snippets to answer my quesiton.'
const commandOutput = await context.editor.controllers?.prompt?.get('output')
const note = 'Refer to the command output, my selected code, 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
const displayText = selection?.fileName ? this.getHumanDisplayText(humanInput, selection?.fileName) : humanInput
Expand Down Expand Up @@ -121,12 +117,12 @@ 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)
contextMessages.push(...currentDirContext)
// 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(...packageJSONContextMessage)
}
contextMessages.push(...currentDirContext)
}
// Create context messages from a fsPath of a workspace directory
if (isContextRequired.directoryPath?.length) {
Expand All @@ -151,7 +147,9 @@ export class MyPrompt implements Recipe {
contextMessages.push(...MyPrompt.getTerminalOutputContext(commandOutput))
}
// Return the last n context messages in case there are too many
return contextMessages.slice(0 - NUM_CODE_RESULTS - NUM_TEXT_RESULTS)
// Make sure numResults is an even number and times 2 again to get the last n pairs
const maxResults = Math.floor((NUM_CODE_RESULTS + NUM_TEXT_RESULTS) / 2) * 2
return contextMessages.slice(-maxResults * 2)
}

// Get context from current editor open tabs
Expand Down Expand Up @@ -198,9 +196,11 @@ export class MyPrompt implements Recipe {
const fileUri = vscode.Uri.file(filePath)
const fileName = vscode.workspace.asRelativePath(filePath)
try {
const content = await vscode.workspace.fs.readFile(fileUri)
const truncatedContent = truncateText(content.toString(), MAX_CURRENT_FILE_TOKENS)
return getContextMessageWithResponse(populateCodeContextTemplate(toJSON(truncatedContent), fileName), {
const bytes = await vscode.workspace.fs.readFile(fileUri)
const decoded = new TextDecoder('utf-8').decode(bytes)
const truncatedContent = truncateText(decoded, MAX_CURRENT_FILE_TOKENS)
// Make sure the truncatedContent is in JSON format
return getContextMessageWithResponse(populateCodeContextTemplate(truncatedContent, fileName), {
fileName,
})
} catch (error) {
Expand Down Expand Up @@ -234,23 +234,29 @@ export class MyPrompt implements Recipe {
// directories, non-test files, and dot files
// then returns the first 10 results
if (testOnly) {
const contextMessages: ContextMessage[] = []
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))
contextMessages.push(...(await populateVscodeDirContextMessage(dirUri, filesInDir)))
if (filesInDir.length > 1) {
return contextMessages
}
const parentDirName = getParentDirName(dirPath)
const fileExt = currentFileName ? getFileExtension(currentFileName) : '*'
// Search for files in directory with test(s) in the name
const testDirFiles = await vscode.workspace.findFiles(`**/test*/**/*.${fileExt}`, undefined, 2)
contextMessages.push(...(await getContextMessageFromFiles(testDirFiles)))
// Search for test files from the parent directory
const testFile = await vscode.workspace.findFiles(
`**/${parentDirName}/**/*test.${fileExt}}`,
'**/node_modules/**',
5
`**/${parentDirName}/**/*test*.${fileExt}}`,
undefined,
2
)
if (testFile.length) {
return await MyPrompt.getFilePathContext(testFile[0].fsPath)
contextMessages.push(...(await getContextMessageFromFiles(testFile)))
// Return the context messages if there are any
if (contextMessages.length) {
return contextMessages
}
}
// Get first 10 files in the directory
Expand All @@ -276,18 +282,16 @@ export class MyPrompt implements Recipe {
try {
const packageJsonUri = packageJsonPath[0]
const packageJsonContent = await vscode.workspace.fs.readFile(packageJsonUri)
const decoded = new TextDecoder('utf-8').decode(packageJsonContent)
// Turn the content into a json and get the scripts object only
const packageJson = JSON.parse(packageJsonContent.toString()) as Record<string, unknown>
const packageJson = JSON.parse(decoded) 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 truncatedContent = truncateText(context.toString() || decoded.toString(), MAX_CURRENT_FILE_TOKENS)
const fileName = vscode.workspace.asRelativePath(packageJsonUri.fsPath)
return getContextMessageWithResponse(populateCodeContextTemplate(toJSON(truncatedContent), fileName), {
return getContextMessageWithResponse(populateCodeContextTemplate(truncatedContent, fileName), {
fileName,
})
} catch {
Expand Down Expand Up @@ -320,8 +324,9 @@ async function populateVscodeDirContextMessage(
continue
}
try {
const fileContent = await vscode.workspace.openTextDocument(fileUri)
const truncatedContent = truncateText(fileContent.getText(), MAX_CURRENT_FILE_TOKENS)
const fileContent = await vscode.workspace.fs.readFile(fileUri)
const decoded = new TextDecoder('utf-8').decode(fileContent)
const truncatedContent = truncateText(decoded, MAX_CURRENT_FILE_TOKENS)
const contextMessage = getContextMessageWithResponse(
populateCurrentEditorContextTemplate(toJSON(truncatedContent), fileName),
{ fileName }
Expand All @@ -337,7 +342,7 @@ async function populateVscodeDirContextMessage(
// 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, '\\/')
const escaped = context.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\//g, '\\/').replace('/\n//', '\n')
return JSON.stringify(escaped)
}

Expand All @@ -356,3 +361,12 @@ const getFirstNFilesFromDir = async (dirUri: vscode.Uri, n: number): Promise<[st
(await vscode.workspace.fs.readDirectory(dirUri))
.filter(file => file[1] === 1 && !file[0].startsWith('.'))
.slice(0, n)

async function getContextMessageFromFiles(files: vscode.Uri[]): Promise<ContextMessage[]> {
const contextMessages: ContextMessage[] = []
for (const file of files) {
const contextMessage = await MyPrompt.getFilePathContext(file.fsPath)
contextMessages.push(...contextMessage)
}
return contextMessages
}
4 changes: 2 additions & 2 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@
{
"command": "cody.customRecipes.list",
"category": "Cody",
"title": "Custom Recipes (Internal Experimental)",
"title": "Custom Recipes (Experimental)",
"when": "cody.activated",
"enablement": "config.cody.experimental.customRecipes",
"icon": "$(bookmark)"
Expand Down Expand Up @@ -844,7 +844,7 @@
"cody.experimental.customRecipes": {
"order": 9,
"type": "boolean",
"markdownDescription": "[Internal Experimental] Create reusable recipes with customized prompts and context tailored to your workflow.",
"markdownDescription": "[Experimental] Create reusable recipes with customized prompts and context tailored to your workflow.",
"default": false
},
"cody.debug.enable": {
Expand Down
14 changes: 3 additions & 11 deletions vscode/resources/samples/user-cody.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,13 @@
"title": "Cody Custom Recipes - User",
"description": "This file showcases how to build custom recipes for Cody by Sourcegraph.",
"recipes": {
"Spell Checker": {
"prompt": "Review the selected code and correct any typos and non-standard usage. Ensure the corrected code remains unchanged in its functionality and readability."
},
"Generate Multiple Unit Tests": {
"prompt": "Create at least 3 full, workable unit tests for the selected code. You may import common libraries for testing. Follow patterns and imports used in existing test files if available, otherwise use best practices for unit testing in this language.",
"prompt": "Generate 2 or more unit tests for the selected code. Provide me with full, workable unit tests. If there are existing test files in the directory, try to follow similar patterns and imports used in those files. You may import common libraries like the language's built-in test framework. If there are no test files, use common best practices for unit testing this language. Make sure to include all the imports needed to run the tests.",
"context": {
"currentDir": true,
"currentFile": true
}
},
"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.",
"context": {
"currentFile": true
}
},
"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.",
Expand Down
7 changes: 4 additions & 3 deletions vscode/src/chat/ChatViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ChatMessage, UserLocalHistory } from '@sourcegraph/cody-shared/src/chat

import { View } from '../../webviews/NavBar'
import { debug } from '../log'
import { CodyPromptType } from '../my-cody/types'
import { logEvent } from '../services/EventLogger'

import { MessageProvider, MessageProviderOptions } from './MessageProvider'
Expand Down Expand Up @@ -93,7 +94,7 @@ export class ChatViewProvider extends MessageProvider implements vscode.WebviewV
void this.openExternalLinks(message.value)
break
case 'my-prompt':
await this.onCustomRecipeClicked(message.title)
await this.onCustomRecipeClicked(message.title, message.value)
break
case 'openFile': {
const rootPath = this.editor.getWorkspaceRootPath()
Expand Down Expand Up @@ -152,13 +153,13 @@ export class ChatViewProvider extends MessageProvider implements vscode.WebviewV
/**
* Process custom recipe click
*/
private async onCustomRecipeClicked(title: string): Promise<void> {
private async onCustomRecipeClicked(title: string, recipeType: CodyPromptType = 'user'): Promise<void> {
this.sendEvent(WebviewEvent.Click, 'custom-recipe')
debug('ChatViewProvider:onCustomRecipeClicked', title)
if (!this.isCustomRecipeAction(title)) {
this.showTab('chat')
}
await this.executeCustomRecipe(title)
await this.executeCustomRecipe(title, recipeType)
}

public showTab(tab: string): void {
Expand Down
12 changes: 6 additions & 6 deletions vscode/src/chat/MessageProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { Message } from '@sourcegraph/cody-shared/src/sourcegraph-api'

import { VSCodeEditor } from '../editor/vscode-editor'
import { debug } from '../log'
import { CodyPromptType } from '../my-cody/types'
import { FixupTask } from '../non-stop/FixupTask'
import { IdleRecipeRunner } from '../non-stop/roles'
import { AuthProvider } from '../services/AuthProvider'
Expand Down Expand Up @@ -529,11 +530,11 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
}

public isCustomRecipeAction(title: string): boolean {
const customRecipeActions = ['add-workspace-file', 'add-user-file', 'get', 'menu']
const customRecipeActions = ['add', 'get', 'menu']
return customRecipeActions.includes(title)
}

public async executeCustomRecipe(title: string): Promise<string | void> {
public async executeCustomRecipe(title: string, type?: CodyPromptType): Promise<string | void> {
if (!this.contextProvider.config.experimentalCustomRecipes) {
return
}
Expand All @@ -548,11 +549,10 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
await this.sendMyPrompts()
return
}
if (title === 'add-workspace-file' || title === 'add-user-file') {
const fileType = title === 'add-workspace-file' ? 'workspace' : 'user'
if (title === 'add' && type) {
try {
// copy the cody.json file from the extension path and move it to the workspace root directory
await this.editor.controllers.prompt?.addJSONFile(fileType)
await this.editor.controllers.prompt?.addJSONFile(type)
} catch (error) {
void vscode.window.showErrorMessage(`Could not create a new cody.json file: ${error}`)
}
Expand All @@ -565,7 +565,7 @@ export abstract class MessageProvider extends MessageHandler implements vscode.D
debug('executeCustomRecipe:noPrompt', title)
return
}
await this.executeCommands(promptText, 'chat-question')
await this.executeCommands(promptText, 'my-prompt')
return promptText
}

Expand Down
3 changes: 2 additions & 1 deletion vscode/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Configuration } from '@sourcegraph/cody-shared/src/configuration'
import { CodyLLMSiteConfiguration } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql/client'

import { View } from '../../webviews/NavBar'
import { CodyPromptType } from '../my-cody/types'

export enum WebviewEvent {
Feedback = 'feedback',
Expand Down Expand Up @@ -37,7 +38,7 @@ export type WebviewMessage =
| { command: 'abort' }
| { command: 'chat-button'; action: string }
| { command: 'setEnabledPlugins'; plugins: string[] }
| { command: 'my-prompt'; title: string }
| { command: 'my-prompt'; title: string; value?: CodyPromptType }

/**
* A message sent from the extension host to the webview.
Expand Down
2 changes: 1 addition & 1 deletion vscode/src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export function getConfiguration(config: ConfigGetter): Configuration {
inlineChat: config.get(CONFIG_KEY.inlineChatEnabled, true),
experimentalGuardrails: config.get(CONFIG_KEY.experimentalGuardrails, isTesting),
experimentalNonStop: config.get('cody.experimental.nonStop' as any, isTesting),
experimentalCustomRecipes: config.get(CONFIG_KEY.experimentalCustomRecipes, false),
experimentalCustomRecipes: config.get(CONFIG_KEY.experimentalCustomRecipes, isTesting),
autocompleteAdvancedProvider,
autocompleteAdvancedServerEndpoint: config.get<string | null>(
CONFIG_KEY.autocompleteAdvancedServerEndpoint,
Expand Down
Loading
Loading