From 73e102814af276e96221641ac589f52d89edd8cd Mon Sep 17 00:00:00 2001 From: arshad Date: Sat, 15 Feb 2025 14:59:29 +0530 Subject: [PATCH] chore: improve prompt --- .gitignore | 2 + packages/core/src/constants.ts | 4 +- packages/monacopilot/src/classes/cache.ts | 62 +++++------- packages/monacopilot/src/classes/formatter.ts | 34 ++++--- packages/monacopilot/src/classes/range.ts | 99 +++++-------------- .../src/classes/text-overlap-calculator.ts | 66 +++++++++++++ packages/monacopilot/src/prompt.ts | 10 +- .../src/utils/inline-completion.ts | 1 - packages/monacopilot/tests/formatter.test.ts | 14 ++- playground/app/api/code-completion/route.ts | 6 +- playground/components/editor.tsx | 1 + 11 files changed, 165 insertions(+), 134 deletions(-) create mode 100644 packages/monacopilot/src/classes/text-overlap-calculator.ts diff --git a/.gitignore b/.gitignore index 5f4237df..b7c3885a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ *.log yalc.lock +.vitepress/cache + .vercel/ .turbo/ .next/ diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 354ac181..9db815bc 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,2 +1,2 @@ -export const DEFAULT_COPILOT_TEMPERATURE = 0.1 as const; -export const DEFAULT_COPILOT_MAX_TOKENS = 64 as const; +export const DEFAULT_COPILOT_TEMPERATURE = 0.5 as const; +export const DEFAULT_COPILOT_MAX_TOKENS = 4096 as const; diff --git a/packages/monacopilot/src/classes/cache.ts b/packages/monacopilot/src/classes/cache.ts index b376dbc6..8b2e751c 100644 --- a/packages/monacopilot/src/classes/cache.ts +++ b/packages/monacopilot/src/classes/cache.ts @@ -4,7 +4,7 @@ import {getTextBeforeCursor} from '../utils/editor'; import {Queue} from './queue'; export class CompletionCache { - private static readonly MAX_CACHE_SIZE = 10; + private static readonly MAX_CACHE_SIZE = 20; private cache: Queue; constructor() { @@ -23,6 +23,9 @@ export class CompletionCache { } public add(cacheItem: Readonly): void { + if (!cacheItem.completion.trim()) { + return; + } this.cache.enqueue(cacheItem); } @@ -38,57 +41,46 @@ export class CompletionCache { const currentRangeValue = mdl.getValueInRange(cacheItem.range); const textBeforeCursor = getTextBeforeCursor(pos, mdl); - // Ensure the current text before cursor hasn't become shorter than the cached prefix - // and that it still starts with the same prefix. - if ( - textBeforeCursor.length < cacheItem.textBeforeCursor.length || - !textBeforeCursor.startsWith(cacheItem.textBeforeCursor) - ) { + if (!textBeforeCursor.includes(cacheItem.textBeforeCursor)) { return false; } - // Then validate the cursor position and typed code against the stored completion range - return this.isPositionValid(cacheItem, pos, currentRangeValue); + if (!this.isPartialMatch(currentRangeValue, cacheItem.completion)) { + return false; + } + + return this.isPositionValid(cacheItem, pos); + } + + private isPartialMatch(current: string, cached: string): boolean { + return cached.startsWith(current) || current.startsWith(cached); } private isPositionValid( cacheItem: Readonly, pos: Readonly, - currentRangeValue: string, ): boolean { - const {range, completion} = cacheItem; + const {range} = cacheItem; const {startLineNumber, startColumn, endLineNumber, endColumn} = range; const {lineNumber, column} = pos; - // Check if the user's current typed text (in the range) is still a valid prefix of the completion. - if (!completion.startsWith(currentRangeValue)) { + const isInLineRange = + lineNumber >= startLineNumber && lineNumber <= endLineNumber; + if (!isInLineRange) { return false; } - // Check if cursor is at the start of the completion range - const isAtStartOfRange = - lineNumber === startLineNumber && column === startColumn; - - // For single-line completion - if (startLineNumber === endLineNumber) { - // Cursor must be at start of the range or within the snippet's boundaries on the same line - return ( - isAtStartOfRange || - (lineNumber === startLineNumber && - column >= startColumn && - column <= endColumn) - ); + if (lineNumber === startLineNumber && lineNumber === endLineNumber) { + return column >= startColumn - 1 && column <= endColumn + 1; } - // For multi-line completion, the cursor must be at start of the range or within - // the vertical bounds. Additionally, if it's on the first or last line, it should - // be within the horizontal bounds; otherwise, any column is acceptable. - const isWithinMultiLineRange = - lineNumber > startLineNumber && lineNumber < endLineNumber - ? true - : (lineNumber === startLineNumber && column >= startColumn) || - (lineNumber === endLineNumber && column <= endColumn); + if (lineNumber === startLineNumber) { + return column >= startColumn - 1; + } + if (lineNumber === endLineNumber) { + return column <= endColumn + 1; + } - return isAtStartOfRange || isWithinMultiLineRange; + return true; } } diff --git a/packages/monacopilot/src/classes/formatter.ts b/packages/monacopilot/src/classes/formatter.ts index 08f64d35..df4c59a7 100644 --- a/packages/monacopilot/src/classes/formatter.ts +++ b/packages/monacopilot/src/classes/formatter.ts @@ -31,18 +31,14 @@ export class CompletionFormatter { } public indentByColumn(): CompletionFormatter { - // Split completion into lines const lines = this.formattedCompletion.split('\n'); - // Skip indentation if there's only one line or if there's text before cursor in the line if (lines.length <= 1 || this.textBeforeCursorInLine.trim() !== '') { return this; } - // Create indentation string based on current column position const indentation = ' '.repeat(this.currentColumn - 1); - // Keep first line as is, indent all subsequent lines this.formattedCompletion = lines[0] + '\n' + @@ -55,16 +51,28 @@ export class CompletionFormatter { } private removeMarkdownCodeBlocks(text: string): string { - const codeBlockRegex = /```[\s\S]*?```/g; - let result = text; - let match: RegExpExecArray | null; - - while ((match = codeBlockRegex.exec(text)) !== null) { - const codeBlock = match[0]; - const codeContent = codeBlock.split('\n').slice(1, -1).join('\n'); - result = result.replace(codeBlock, codeContent); + const lines = text.split('\n'); + const result: string[] = []; + let inCodeBlock = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const isCodeBlockStart = line.trim().startsWith('```'); + + if (isCodeBlockStart && !inCodeBlock) { + inCodeBlock = true; + continue; + } + + if (isCodeBlockStart && inCodeBlock) { + inCodeBlock = false; + continue; + } + + result.push(line); } - return result; + + return result.join('\n'); } public removeExcessiveNewlines(): CompletionFormatter { diff --git a/packages/monacopilot/src/classes/range.ts b/packages/monacopilot/src/classes/range.ts index c35c7fb4..87d31d01 100644 --- a/packages/monacopilot/src/classes/range.ts +++ b/packages/monacopilot/src/classes/range.ts @@ -4,97 +4,43 @@ import type { EditorRange, Monaco, } from '../types/monaco'; +import {TextOverlapCalculator} from './text-overlap-calculator'; export class CompletionRange { - constructor(private monaco: Monaco) {} + private textOverlapCalculator: TextOverlapCalculator; + + constructor(private monaco: Monaco) { + this.textOverlapCalculator = new TextOverlapCalculator(); + } public computeInsertionRange( pos: CursorPosition, completion: string, mdl: EditorModel, ): EditorRange { - // Handle empty completion if (!completion) { - return new this.monaco.Range( - pos.lineNumber, - pos.column, - pos.lineNumber, - pos.column, - ); + return this.createEmptyRange(pos); } const startOffset = mdl.getOffsetAt(pos); const textBeforeCursor = mdl.getValue().substring(0, startOffset); const textAfterCursor = mdl.getValue().substring(startOffset); - let prefixOverlapLength = 0; - let suffixOverlapLength = 0; - let maxOverlapLength = 0; - let startOverlapLength = 0; - - const completionLength = completion.length; - const beforeLength = textBeforeCursor.length; - const afterLength = textAfterCursor.length; - - // Handle cursor at the end of the document if (startOffset >= mdl.getValue().length) { - return new this.monaco.Range( - pos.lineNumber, - pos.column, - pos.lineNumber, - pos.column, - ); + return this.createEmptyRange(pos); } - // Handle empty remaining text - if (afterLength === 0) { - return new this.monaco.Range( - pos.lineNumber, - pos.column, - pos.lineNumber, - pos.column, - ); - } - - // Find overlap with text before cursor - const maxBeforeOverlap = Math.min(completionLength, beforeLength); - for (let i = 1; i <= maxBeforeOverlap; i++) { - const completionStart = completion.substring(0, i); - const textEnd = textBeforeCursor.slice(-i); - if (completionStart === textEnd) { - startOverlapLength = i; - } + if (textAfterCursor.length === 0) { + return this.createEmptyRange(pos); } - // Find overlap with text after cursor - const maxAfterOverlap = Math.min(completionLength, afterLength); - - // Find the longest prefix overlap with text after cursor - for (let i = 0; i < maxAfterOverlap; i++) { - if (completion[i] !== textAfterCursor[i]) break; - prefixOverlapLength++; - } - - // Find the longest suffix overlap with text after cursor - for (let i = 1; i <= maxAfterOverlap; i++) { - if (completion.slice(-i) === textAfterCursor.slice(0, i)) { - suffixOverlapLength = i; - } - } - - maxOverlapLength = Math.max(prefixOverlapLength, suffixOverlapLength); - - // Check for internal overlaps if no prefix or suffix overlap - if (maxOverlapLength === 0) { - for (let i = 1; i < completionLength; i++) { - if (textAfterCursor.startsWith(completion.substring(i))) { - maxOverlapLength = completionLength - i; - break; - } - } - } + const {startOverlapLength, maxOverlapLength} = + this.textOverlapCalculator.findOverlaps( + completion, + textBeforeCursor, + textAfterCursor, + ); - // Calculate start and end positions const startPosition = startOverlapLength > 0 ? mdl.getPositionAt(startOffset - startOverlapLength) @@ -122,8 +68,8 @@ export class CompletionRange { const endLineNumber = startLineNumber + lastLineIndex; const endColumn = lastLineIndex === 0 - ? startColumn + completionLines[0].length // Single-line completion - : completionLines[lastLineIndex].length + 1; // Multi-line completion + ? startColumn + completionLines[0].length + : completionLines[lastLineIndex].length + 1; return new this.monaco.Range( startLineNumber, @@ -132,4 +78,13 @@ export class CompletionRange { endColumn, ); } + + private createEmptyRange(pos: CursorPosition): EditorRange { + return new this.monaco.Range( + pos.lineNumber, + pos.column, + pos.lineNumber, + pos.column, + ); + } } diff --git a/packages/monacopilot/src/classes/text-overlap-calculator.ts b/packages/monacopilot/src/classes/text-overlap-calculator.ts new file mode 100644 index 00000000..0128d5dc --- /dev/null +++ b/packages/monacopilot/src/classes/text-overlap-calculator.ts @@ -0,0 +1,66 @@ +export class TextOverlapCalculator { + public findOverlaps( + completion: string, + textBeforeCursor: string, + textAfterCursor: string, + ): { + startOverlapLength: number; + maxOverlapLength: number; + } { + if (!completion) { + return { + startOverlapLength: 0, + maxOverlapLength: 0, + }; + } + + const completionLength = completion.length; + const beforeLength = textBeforeCursor.length; + const afterLength = textAfterCursor.length; + + let prefixOverlapLength = 0; + let suffixOverlapLength = 0; + let startOverlapLength = 0; + + const maxBeforeOverlap = Math.min(completionLength, beforeLength); + for (let i = 1; i <= maxBeforeOverlap; i++) { + const completionStart = completion.substring(0, i); + const textEnd = textBeforeCursor.slice(-i); + if (completionStart === textEnd) { + startOverlapLength = i; + } + } + + const maxAfterOverlap = Math.min(completionLength, afterLength); + + for (let i = 0; i < maxAfterOverlap; i++) { + if (completion[i] !== textAfterCursor[i]) break; + prefixOverlapLength++; + } + + for (let i = 1; i <= maxAfterOverlap; i++) { + if (completion.slice(-i) === textAfterCursor.slice(0, i)) { + suffixOverlapLength = i; + } + } + + let maxOverlapLength = Math.max( + prefixOverlapLength, + suffixOverlapLength, + ); + + if (maxOverlapLength === 0) { + for (let i = 1; i < completionLength; i++) { + if (textAfterCursor.startsWith(completion.substring(i))) { + maxOverlapLength = completionLength - i; + break; + } + } + } + + return { + startOverlapLength, + maxOverlapLength, + }; + } +} diff --git a/packages/monacopilot/src/prompt.ts b/packages/monacopilot/src/prompt.ts index b02a61bb..92c7ba3a 100644 --- a/packages/monacopilot/src/prompt.ts +++ b/packages/monacopilot/src/prompt.ts @@ -11,14 +11,10 @@ const compileRelatedFiles = (files?: RelatedFile[]): string => { return files .map(({path, content}) => { return ` - - ${path} - +### ${path} \`\`\` ${content} -\`\`\` - -`.trim(); +\`\`\``.trim(); }) .join('\n\n'); }; @@ -60,7 +56,7 @@ ${textBeforeCursor}${textAfterCursor} ${capitalizeFirstLetter(completionMode)} the code at . -Output only the raw code to be inserted at the cursor location without any additional text or comments.`; +Output only the raw code to be inserted at the cursor location without any additional text, comments, or code block syntax.`; return { system: systemInstruction, diff --git a/packages/monacopilot/src/utils/inline-completion.ts b/packages/monacopilot/src/utils/inline-completion.ts index cf342d96..dd1fd612 100644 --- a/packages/monacopilot/src/utils/inline-completion.ts +++ b/packages/monacopilot/src/utils/inline-completion.ts @@ -8,5 +8,4 @@ export const createInlineCompletionResult = ( ): EditorInlineCompletionsResult => ({ items, enableForwardStability: true, - suppressSuggestions: true, }); diff --git a/packages/monacopilot/tests/formatter.test.ts b/packages/monacopilot/tests/formatter.test.ts index 854f4af1..bfabb961 100644 --- a/packages/monacopilot/tests/formatter.test.ts +++ b/packages/monacopilot/tests/formatter.test.ts @@ -62,6 +62,18 @@ describe('CompletionFormatter', () => { ); }); + it('should remove markdown code block in the middle of the text', () => { + const formatter = new CompletionFormatter( + 'calculate the factorial of a given number using recursion\n```javascript\nfunction calculateFactorial(n) {\n // Base case: factorial of 0 or 1 is 1\n if (n === 0 || n === 1) {\n return 1;\n ', + MOCK_COMPLETION_POS.column, + '', + ); + const result = formatter.removeMarkdownCodeSyntax().build(); + expect(result).toBe( + 'calculate the factorial of a given number using recursion\nfunction calculateFactorial(n) {\n // Base case: factorial of 0 or 1 is 1\n if (n === 0 || n === 1) {\n return 1;\n ', + ); + }); + it('should remove multiple markdown code blocks', () => { const formatter = new CompletionFormatter( '```\nfunction greet(name) {\n return `Hello, ${name}!`;\n}\n```\nSome text\n```\nconst result = greet("Alice");\nconsole.log(result);\n```', @@ -113,7 +125,7 @@ describe('CompletionFormatter', () => { '', ); const result = formatter.removeMarkdownCodeSyntax().build(); - expect(result).toBe('```\nconst incomplete = true;'); + expect(result).toBe('const incomplete = true;'); }); it('should keep starting whitespaces', () => { diff --git a/playground/app/api/code-completion/route.ts b/playground/app/api/code-completion/route.ts index 22723910..f097c862 100644 --- a/playground/app/api/code-completion/route.ts +++ b/playground/app/api/code-completion/route.ts @@ -2,9 +2,9 @@ import {NextRequest, NextResponse} from 'next/server'; import {CompletionCopilot} from 'monacopilot'; -const copilot = new CompletionCopilot(process.env.OPENAI_API_KEY, { - provider: 'openai', - model: 'gpt-4o-mini', +const copilot = new CompletionCopilot(process.env.GROQ_API_KEY, { + provider: 'groq', + model: 'llama-3-70b', }); export async function POST(req: NextRequest) { diff --git a/playground/components/editor.tsx b/playground/components/editor.tsx index 666d1c64..e63baf6e 100644 --- a/playground/components/editor.tsx +++ b/playground/components/editor.tsx @@ -20,6 +20,7 @@ const Editor = () => { const completion = registerCompletion(monaco, editor, { endpoint: '/api/code-completion', language: 'javascript', + trigger: 'onTyping', }); return () => {