From 2fffd577af7655cea0f078bfa83b7fef9b35c8fd Mon Sep 17 00:00:00 2001 From: Gordon Mickel Date: Tue, 6 Aug 2024 00:06:15 +0200 Subject: [PATCH] feat: Add --no-plan mode for streamlined code generatio (#77) * prompting: further improve prompts * feat: add --no-plan mode --- USAGE.md | 5 +- src/ai/task-workflow.ts | 125 +++++++++--- src/cli/index.ts | 9 + src/templates/codegen-diff-no-plan-prompt.hbs | 180 ++++++++++++++++++ src/templates/codegen-diff-prompt.hbs | 17 +- src/templates/codegen-no-plan-prompt.hbs | 163 ++++++++++++++++ src/templates/codegen-prompt.hbs | 19 +- src/types/index.ts | 1 + tests/unit/task-workflow.test.ts | 134 +++++++++++++ 9 files changed, 609 insertions(+), 44 deletions(-) create mode 100644 src/templates/codegen-diff-no-plan-prompt.hbs create mode 100644 src/templates/codegen-no-plan-prompt.hbs diff --git a/USAGE.md b/USAGE.md index 64bcfeb..e09b0e0 100644 --- a/USAGE.md +++ b/USAGE.md @@ -53,7 +53,10 @@ codewhisper task [options] | `-c, --context ` | Specify files or directories to include in the task context. Can be file paths, directory paths, or glob patterns. Multiple entries should be space-separated. | | `--github-issue` | Use GitHub issue for task input | | `--github-issue-filters ` | Use these filters when fetching issues. Format: comma-separated key:value pairs. Example: labels:p1,assignee:abc Note: see "query parameters" at https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues--parameters for all options. | -| `-df, --diff` | Use diff format for file updates instead of full file content | +| `-df, --diff` | Use diff format for file updates instead of full file content (default: true) | +| `--no-diff` | Disable diff mode (default: false) | +| `--plan` | Use the planning mode, this generates an intermediate plan, which can be modified. Useful for complex tasks. (default: true) | +| `--no-plan` | Disable the planning mode. Useful for simple tasks (default: false) | | `-g, --gitignore ` | Path to .gitignore file (default: .gitignore) | | `-f, --filter ` | File patterns to include (use glob patterns, e.g., "src/\*_/_.js") | | `-e, --exclude ` | File patterns to exclude (use glob patterns, e.g., "\*_/_.test.js") | diff --git a/src/ai/task-workflow.ts b/src/ai/task-workflow.ts index 252c869..1e67416 100644 --- a/src/ai/task-workflow.ts +++ b/src/ai/task-workflow.ts @@ -64,32 +64,49 @@ export async function runAIAssistedTask(options: AiAssistedTaskOptions) { ); spinner.succeed('Files processed successfully'); - const planPrompt = await generatePlanPrompt( - processedFiles, - templateContent, - customData, - options, - basePath, - ); + if (options.plan) { + const planPrompt = await generatePlanPrompt( + processedFiles, + templateContent, + customData, + options, + basePath, + ); - const generatedPlan = await generatePlan(planPrompt, modelKey, options); + const generatedPlan = await generatePlan(planPrompt, modelKey, options); - // Cache the task data - taskCache.setTaskData(basePath, { - selectedFiles, - generatedPlan, - taskDescription, - instructions, - model: modelKey, - }); + // Cache the task data + taskCache.setTaskData(basePath, { + selectedFiles, + generatedPlan, + taskDescription, + instructions, + model: modelKey, + }); + + await continueTaskWorkflow( + options, + basePath, + taskCache, + generatedPlan, + modelKey, + ); + } else { + taskCache.setTaskData(basePath, { + selectedFiles, + generatedPlan: '', + taskDescription, + instructions, + model: modelKey, + }); - await continueTaskWorkflow( - options, - basePath, - taskCache, - generatedPlan, - modelKey, - ); + await continueTaskWorkflowWithoutPlan( + options, + basePath, + taskCache, + modelKey, + ); + } } catch (error) { spinner.fail('Error in AI-assisted task'); console.error( @@ -336,7 +353,60 @@ export async function continueTaskWorkflow( codegenTemplateContent, taskCache, basePath, + options, reviewedPlan, + ); + + spinner.start('Generating Codegen prompt...'); + const codeGenPrompt = await generateCodegenPrompt( + options, + basePath, + taskCache, + codegenTemplateContent, + codegenCustomData, + ); + spinner.succeed('Codegen prompt generated successfully'); + + const generatedCode = await generateCode(codeGenPrompt, modelKey, options); + const parsedResponse = parseAICodegenResponse( + generatedCode, + options.logAiInteractions, + options.diff, + ); + + if (options.dryRun) { + await handleDryRun( + basePath, + parsedResponse, + taskCache.getLastTaskData(basePath)?.taskDescription || '', + ); + } else { + await applyCodeModifications(options, basePath, parsedResponse); + } + + spinner.succeed('AI-assisted task completed! 🎉'); +} + +export async function continueTaskWorkflowWithoutPlan( + options: AiAssistedTaskOptions, + basePath: string, + taskCache: TaskCache, + modelKey: string, +) { + const spinner = ora(); + + const codegenTemplatePath = options.diff + ? getTemplatePath('codegen-diff-no-plan-prompt') + : getTemplatePath('codegen-no-plan-prompt'); + + const codegenTemplateContent = await fs.readFile( + codegenTemplatePath, + 'utf-8', + ); + const codegenCustomData = await prepareCodegenCustomData( + codegenTemplateContent, + taskCache, + basePath, options, ); @@ -374,8 +444,8 @@ async function prepareCodegenCustomData( codegenTemplateContent: string, taskCache: TaskCache, basePath: string, - reviewedPlan: string, options: AiAssistedTaskOptions, + reviewedPlan?: string, ): Promise> { const codegenVariables = extractTemplateVariables(codegenTemplateContent); const lastTaskData = taskCache.getLastTaskData(basePath); @@ -383,8 +453,11 @@ async function prepareCodegenCustomData( const codegenDataObj = { var_taskDescription: lastTaskData?.taskDescription || '', var_instructions: lastTaskData?.instructions || '', - var_plan: reviewedPlan, - }; + } as Record; + + if (reviewedPlan) { + codegenDataObj.var_plan = reviewedPlan; + } return collectVariables( JSON.stringify(codegenDataObj), diff --git a/src/cli/index.ts b/src/cli/index.ts index b8f4aa2..2714a5b 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -94,6 +94,15 @@ export function cli(_args: string[]) { true, ) .option('--no-diff', 'Use full file content for updates') + .option( + '--plan', + 'Use the plan mode for AI-generated code modifications', + true, + ) + .option( + '--no-plan', + 'Directly provide the code modifications without the intermediate planning step', + ) .option( '-cw, --context-window ', 'Specify the context window for the AI model. Only applicable for Ollama models.', diff --git a/src/templates/codegen-diff-no-plan-prompt.hbs b/src/templates/codegen-diff-no-plan-prompt.hbs new file mode 100644 index 0000000..cd4e9b5 --- /dev/null +++ b/src/templates/codegen-diff-no-plan-prompt.hbs @@ -0,0 +1,180 @@ +You are an expert developer tasked with implementing a given task. Your goal is to write all the code changes needed to complete the task, ensuring it integrates well with the existing codebase and follows best practices. + +You will be given: +- A task description +- A codebase +- Instructions + +Before implementing the task, carefully analyze the task description, instructions, and codebase. Plan your implementation strategy, considering best practices, coding standards, potential edge cases, and performance implications. Approach this task as a senior developer working on a critical feature for a high-profile client. + +Note: Focus solely on the technical implementation. Ignore any mentions of human tasks or non-technical aspects. + +Encoded in XML tags, here is what you will be given: + +TASK: Context about the task to complete. +INSTRUCTIONS: Instructions on how to complete the task. +CODEBASE: Files from the codebase you have access to. +FORMAT: Instructions for how to format your response. + +--- + + + {{var_taskDescription}} + + +--- + + + Follow these instructions: + + {{var_instructions}} + + +--- + + + ## Code Summary + + {{tableOfContents files}} + + ## Selected Files: + {{#each files}} + ### {{relativePath this.path}} + + {{#codeblock this.content this.language}}{{/codeblock}} + + {{/each}} + + + +--- + + + + Generate diffs for modified files, full content for new files, and only the file path for deleted files. + + If you don't need to modify a file, don't include it - this simplifies Git diffs. + + Format your response as follows: + + FILE_PATH_1 + FILE_PATH_2 + ... + + + __GIT_BRANCH_NAME__ + + + + __GIT_COMMIT_MESSAGE__ + + + + __BRIEF_SUMMARY_OF_CHANGES__ + + + + __LIST_OF_POTENTIAL_ISSUES_OR_TRADE_OFFS__ + Include any constraints (e.g., performance, scalability, maintainability) you've considered and explain why the trade-offs you've made are appropriate for this task. + + + Then, for each file: + + __FILE_PATH__ + __STATUS__ + + __FILE_CONTENT_OR_DIFF__ + + + __EXPLANATION__ + + + + Please adhere to the following guidelines: + + FILE_PATH: Use the full path from the project root. + Example: 'src/components/Button.tsx' + + LANGUAGE: Specify the language or file type. For example: + 'tsx' for .tsx files + 'javascript' for .js files + 'css' for .css files + 'json' for .json files + etc + + FILE_CONTENT_OR_DIFF: + - For new files: Provide the complete file content, including all necessary imports, function definitions, and exports. + - For modified files: Provide a unified diff format. Use '---' for removed lines and '+++' for added lines. + - For deleted files: Leave this section empty. + + Ensure proper indentation and follow the project's coding standards. + + STATUS: Use 'new' for newly created files, 'modified' for existing files that are being updated, and 'deleted' for files that are being deleted. + + EXPLANATION: Provide a brief explanation for your implementation choices, including any significant design decisions, alternatives considered, and reasons for your final decision. Address any non-obvious implementations or optimizations. + + When creating diffs for modified files: + - Use '+' at the beginning of the line to indicate added lines, including new imports. + - Use '-' at the beginning of the line to indicate removed lines. + - Use ' ' (space) at the beginning of the line for unchanged lines (context). + - Ensure that new imports are marked with '+' at the beginning of the line. + - Include at least 3 lines of unchanged context before and after changes to help with patch application. + + Example of a correct diff format: + + --- src/types/index.ts + +++ src/types/index.ts + @@ -1,4 +1,6 @@ + import type { ParsedDiff } from 'diff'; + +import type { LogLevel } from '../utils/logger'; + + + export interface GitHubIssue { + number: number; + title: string; + @@ -40,6 +42,7 @@ + | 'noCodeblock' + > & { + dryRun: boolean; + + logLevel?: LogLevel; + maxCostThreshold?: number; + task?: string; + description?: string; + + + Before modifying a file, carefully review its entire content. Ensure that your changes, especially new imports, are placed in the correct location and don't duplicate existing code. + + When generating diffs: + 1. Start with the original file content. + 2. Make your changes, keeping track of line numbers. + 3. Generate the diff by comparing the original and modified versions. + 4. Include at least 3 lines of unchanged context before and after changes. + 5. Verify that the diff accurately represents your intended changes. + + After generating each diff: + - Verify that all new lines (including imports and blank lines) start with '+'. + - Ensure that the line numbers in the diff headers (@@ -old,oldlines +new,newlines @@) are correct and account for added/removed lines. + - Check that there are sufficient unchanged context lines around modifications. + + Ensure that: + - You have thoroughly analyzed the task and planned your implementation strategy. + - Everything specified in the task description and instructions is implemented. + - All new files contain the full code. + - All modified files have accurate and clear diffs. + - The content includes all necessary imports, function definitions, and exports. + - The code is clean, maintainable, efficient, and considers performance implications. + - The code is properly formatted and follows the project's coding standards. + - Necessary comments for clarity are included if needed. + - Any conceptual or high-level descriptions are translated into actual, executable code. + - You've considered and handled potential edge cases. + - Your changes are consistent with the existing codebase. + - You haven't introduced any potential bugs or performance issues. + - Your code is easy to understand and maintain. + - You complete all necessary work to fully implement the task. + + Note: The accuracy of the diff format is crucial for successful patch application. Even small errors in formatting can cause the entire patch to fail. Pay extra attention to the correctness of your diff output. + + + +--- + +Now, implement the task described above. Take your time to think through the problem and craft an elegant, efficient, and complete solution that fully addresses the task requirements and integrates seamlessly with the existing codebase. diff --git a/src/templates/codegen-diff-prompt.hbs b/src/templates/codegen-diff-prompt.hbs index e09c0ea..5e09490 100644 --- a/src/templates/codegen-diff-prompt.hbs +++ b/src/templates/codegen-diff-prompt.hbs @@ -1,5 +1,3 @@ -# AI Expert Developer - You are an expert developer tasked with implementing a given task. Your goal is to write all the code changes needed to complete the task, ensuring it integrates well with the existing codebase and follows best practices. @@ -228,21 +226,22 @@ FORMAT: Instructions for how to format your response. Ensure that: - - Everything specified in the plan is implemented. + - You have thoroughly analyzed the task and plan and have planned your implementation strategy. + - Everything specified in the task description and plan is implemented. - All new files contain the full code. - All modified files have accurate and clear diffs. + - Fiff formatting across all modified files is consistent. - The content includes all necessary imports, function definitions, and exports. - The code is clean, maintainable, and efficient. - The code is properly formatted and follows the project's coding standards. - Necessary comments for clarity are included if needed. + - Any conceptual or high-level descriptions are translated into actual, executable code. + - You've considered and handled potential edge cases. + - Your changes are consistent with the existing codebase. + - You haven't introduced any potential bugs or performance issues. + - Your code is easy to understand and maintain. - You complete all necessary work. - After completing all changes, review your entire output. Check for: - - Consistency in diff formatting across all modified files. - - Accuracy of file paths and language specifications. - - Completeness of implementations as per the plan. - - Potential conflicts or inconsistencies between different file changes. - Note: The accuracy of the diff format is crucial for successful patch application. Even small errors in formatting can cause the entire patch to fail. Pay extra attention to the correctness of your diff output. diff --git a/src/templates/codegen-no-plan-prompt.hbs b/src/templates/codegen-no-plan-prompt.hbs new file mode 100644 index 0000000..f21f248 --- /dev/null +++ b/src/templates/codegen-no-plan-prompt.hbs @@ -0,0 +1,163 @@ +You are an expert developer tasked with implementing a given task. Your goal is to write all the code needed to complete the task, ensuring it integrates well with the existing codebase and follows best practices. + +You will be given: +- A task description +- A codebase +- Instructions + +Before implementing the task, carefully analyze the task description, instructions, and codebase. Plan your implementation strategy, considering best practices, coding standards, potential edge cases, and performance implications. Approach this task as a senior developer working on a critical feature for a high-profile client. + +Note: Focus solely on the technical implementation. Ignore any mentions of human tasks or non-technical aspects. + +Encoded in XML tags, here is what you will be given: + +TASK: Context about the task to complete. +INSTRUCTIONS: Instructions on how to complete the task. +CODEBASE: Files from the codebase you have access to. +FORMAT: Instructions for how to format your response. + +--- + + + {{var_taskDescription}} + + +--- + + + Follow these instructions: + + {{var_instructions}} + + +--- + + + ## Code Summary + + {{tableOfContents files}} + + ## Selected Files: + {{#each files}} + ### {{relativePath this.path}} + + {{#codeblock this.content this.language}}{{/codeblock}} + + {{/each}} + + + +--- + + + + Always generate the full content for each new or modified file. + + Only provide the full path for each deleted file. + + If you don't need to modify a file, don't include it - this simplifies Git diffs. + + Format your response as follows: + + FILE_PATH_1 + FILE_PATH_2 + ... + + + __GIT_BRANCH_NAME__ + + + + __GIT_COMMIT_MESSAGE__ + + + + __BRIEF_SUMMARY_OF_CHANGES__ + + + + __LIST_OF_POTENTIAL_ISSUES_OR_TRADE_OFFS__ + Include any constraints (e.g., performance, scalability, maintainability) you've considered and explain why the trade-offs you've made are appropriate for this task. + + + Then, for each file: + + __FILE_PATH__ + + __FILE_CONTENT__ + + __STATUS__ + + __EXPLANATION__ + + + + Please adhere to the following guidelines: + + FILE_PATH: Use the full path from the project root. + Example: 'components/Button.tsx' + + LANGUAGE: Specify the language or file type. For example: + 'tsx' for .tsx files + 'javascript' for .js files + 'css' for .css files + 'json' for .json files + etc + + FILE_CONTENT: Provide the complete file content, including all necessary imports, function definitions, and exports. + Ensure proper indentation and follow the project's coding standards. + + STATUS: Use 'new' for newly created files, 'modified' for existing files that are being updated, and 'deleted' for + files that are being deleted. + + EXPLANATION: Provide a brief explanation for your implementation choices, including any significant design decisions, alternatives considered, and reasons for your final decision. Address any non-obvious implementations or optimizations. + + Example: + + components/IssueList.tsx + + import React from 'react'; + import { Issue } from '../types'; + + interface IssueListProps { + issues: Issue[]; + } + + export const IssueList: React.FC = ({ issues }) => { + return ( +
    + {issues.map((issue) => ( +
  • {issue.title}
  • + ))} +
+ ); + }; +
+ new + + Created a new IssueList component to display a list of issues. Used React.FC for type-safety and map function for + efficient rendering of multiple issues. Considered using a table instead of a list but opted for a list for simplicity and better mobile responsiveness. + +
+ + Ensure that: + - You have thoroughly analyzed the task and planned your implementation strategy. + - Everything specified in the task description and instructions is implemented. + - All new or modified files contain the full code. + - Regardless of the task complexity, all new or modified files must be 100% complete. + - The content includes all necessary imports, function definitions, and exports. + - The code is clean, maintainable, efficient, and considers performance implications. + - The code is properly formatted and follows the project's coding standards. + - Necessary comments for clarity are included if needed. + - Any conceptual or high-level descriptions are translated into actual, executable code. + - You've considered and handled potential edge cases. + - Your changes are consistent with the existing codebase. + - You haven't introduced any potential bugs or performance issues. + - Your code is easy to understand and maintain. + - You complete all necessary work to fully implement the task. + +
+ +--- + +Now, implement the task described above. Take your time to think through the problem and craft an elegant, efficient, and complete solution that fully addresses the task requirements and integrates seamlessly with the existing codebase. diff --git a/src/templates/codegen-prompt.hbs b/src/templates/codegen-prompt.hbs index 34a6538..815e2e8 100644 --- a/src/templates/codegen-prompt.hbs +++ b/src/templates/codegen-prompt.hbs @@ -1,5 +1,3 @@ -# AI Expert Developer - You are an expert developer tasked with implementing a given task. Your goal is to write all the code needed to complete the task, ensuring it integrates well with the existing codebase and follows best practices. @@ -152,14 +150,19 @@ FORMAT: Instructions for how to format your response. Ensure that: - - Everything specified in the plan is implemented. - - All new or modified file contains the full code. - - Regardless of the plan, all new or modified files must be 100% complete. + - You have thoroughly analyzed the task and plan and have planned your implementation strategy. + - Everything specified in the task description and plan is implemented. + - All new or modified files contain the full code. + - Regardless of the task complexity, all new or modified files must be 100% complete. - The content includes all necessary imports, function definitions, and exports. - - The code is clean, maintainable, and efficient. + - The code is clean, maintainable, efficient, and considers performance implications. - The code is properly formatted and follows the project's coding standards. - Necessary comments for clarity are included if needed. - - Pseudocode is translated into actual code. - - You complete all necessary work. + - Any conceptual or high-level descriptions are translated into actual, executable code. + - You've considered and handled potential edge cases. + - Your changes are consistent with the existing codebase. + - You haven't introduced any potential bugs or performance issues. + - Your code is easy to understand and maintain. + - You complete all necessary work to fully implement the task. diff --git a/src/types/index.ts b/src/types/index.ts index 55d17d4..ca73eb4 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -64,6 +64,7 @@ export type AiAssistedTaskOptions = Pick< githubIssueFilters?: string; issueNumber?: number; diff?: boolean; + plan?: boolean; context?: string[]; }; diff --git a/tests/unit/task-workflow.test.ts b/tests/unit/task-workflow.test.ts index 4be1936..68d47b7 100644 --- a/tests/unit/task-workflow.test.ts +++ b/tests/unit/task-workflow.test.ts @@ -63,6 +63,7 @@ describe('runAIAssistedTask', () => { dryRun: false, noCodeblock: false, invert: false, + plan: true, }; beforeEach(() => { @@ -73,6 +74,139 @@ describe('runAIAssistedTask', () => { vi.clearAllMocks(); }); + it('should execute the happy path successfully with no planning step', async () => { + const mockTaskDescription = 'Test task description'; + const mockInstructions = 'Test instructions'; + const mockSelectedFiles = ['file1.ts', 'file2.ts']; + const mockProcessedFiles = [ + { + path: 'file1.ts', + content: 'content1', + language: 'typescript', + size: 100, + created: new Date(), + modified: new Date(), + extension: 'ts', + }, + { + path: 'file2.ts', + content: 'content2', + language: 'typescript', + size: 200, + created: new Date(), + modified: new Date(), + extension: 'ts', + }, + ]; + const mockGeneratedCode = ` + + file1.ts + file2.ts + + + file1.ts + + // Updated content for file1.ts + + modified + + + file2.ts + + // Updated content for file2.ts + + modified + + feature/test-task + Implement test task + Updated both files + None + `; + + const mockParsedResponse = { + gitBranchName: 'feature/test-task', + gitCommitMessage: 'Implement test task', + fileList: ['file1.ts', 'file2.ts'], + files: [ + { + path: 'file1.ts', + content: '// Updated content for file1.ts', + language: 'typescript', + status: 'modified', + }, + { + path: 'file2.ts', + content: '// Updated content for file2.ts', + language: 'typescript', + status: 'modified', + }, + ], + summary: 'Updated both files', + potentialIssues: 'None', + }; + + const mockModelConfig: ModelSpec = { + contextWindow: 100000, + maxOutput: 4096, + modelName: 'Claude 3.5 Sonnet', + pricing: { inputCost: 3, outputCost: 15 }, + modelFamily: 'claude', + temperature: { + planningTemperature: 0.5, + codegenTemperature: 0.3, + }, + }; + + vi.mocked(getTaskDescription).mockResolvedValue(mockTaskDescription); + vi.mocked(getInstructions).mockResolvedValue(mockInstructions); + vi.mocked(selectFilesPrompt).mockResolvedValue(mockSelectedFiles); + vi.mocked(processFiles).mockResolvedValue(mockProcessedFiles); + vi.mocked(generateMarkdown).mockResolvedValue('Generated markdown'); + vi.mocked(generateAIResponse).mockResolvedValueOnce(mockGeneratedCode); + vi.mocked(parseAICodegenResponse).mockReturnValue( + mockParsedResponse as unknown as AIParsedResponse, + ); + vi.mocked(ensureBranch).mockResolvedValue('feature/test-task'); + vi.mocked(applyChanges).mockResolvedValue(); + vi.mocked(getModelConfig).mockReturnValue(mockModelConfig); + + const mockOptionsWithoutPlan = { + ...mockOptions, + plan: false, + }; + + await runAIAssistedTask(mockOptionsWithoutPlan); + + expect(getTaskDescription).toHaveBeenCalled(); + expect(getInstructions).toHaveBeenCalled(); + expect(processFiles).toHaveBeenCalledWith( + expect.objectContaining({ + path: expect.stringMatching(/[\\\/]test[\\\/]path$/), + }), + ); + expect(generateMarkdown).toHaveBeenCalledTimes(1); + expect(generateAIResponse).toHaveBeenCalledTimes(1); + expect(reviewPlan).toHaveBeenCalledTimes(0); + expect(parseAICodegenResponse).toHaveBeenCalledWith( + mockGeneratedCode, + undefined, + undefined, + ); + + expect(ensureBranch).toHaveBeenCalledWith( + expect.stringMatching(/[\\\/]test[\\\/]path$/), + 'feature/test-task', + { issueNumber: undefined }, + ); + expect(applyChanges).toHaveBeenCalledWith( + expect.objectContaining({ + basePath: expect.stringMatching(/[\\\/]test[\\\/]path$/), + parsedResponse: mockParsedResponse, + dryRun: false, + }), + ); + }); + it('should execute the happy path successfully', async () => { const mockTaskDescription = 'Test task description'; const mockInstructions = 'Test instructions';