server/aws-lsp-codewhisperer/src/language-server/inline-completion/auto-trigger/autoTrigger.ts (125 lines of code) (raw):

import { FileContext } from '../../../shared/codeWhispererService' import typedCoefficients = require('./coefficients.json') type TypedCoefficients = typeof typedCoefficients type Coefficients = TypedCoefficients & { [K in keyof TypedCoefficients]: TypedCoefficients[K] extends number ? TypedCoefficients[K] : Record<string, number> } /** * Add a string indexer to each category coefficient so we can accept string inputs * from consumers while maintaining type safety inside the auto-trigger * (and prevent accidental deletions and typos in the json file for non-category coefficients) */ const coefficients: Coefficients = typedCoefficients as Coefficients // The threshold to trigger an auto-trigger based on the sigmoid of the result const TRIGGER_THRESHOLD = 0.43 // The sigmoid function to clamp the auto-trigger result to the (0, 1) range const sigmoid = (x: number) => { return 1 / (1 + Math.exp(-x)) } // Check if a character or a single character insertion (sometimes expanded to matching character for brackets) // are special characters. This could be expanded if more languages with other special characters are supported. const isSpecialCharacter = (char: string) => ['(', '()', '[', '[]', '{', '{}', ':'].includes(char) export type CodewhispererTriggerType = 'AutoTrigger' | 'OnDemand' // Two triggers are explicitly handled, SpecialCharacters and Enter. Everything else is expected to be a trigger // based on regular typing, and is considered a 'Classifier' trigger. export type CodewhispererAutomatedTriggerType = 'SpecialCharacters' | 'Enter' | 'Classifier' /** * Determine the trigger type based on the file context. Currently supports special cases for Special Characters and Enter keys, * as determined by the File Context. For regular typing or undetermined triggers, the Classifier trigger type is used. * * This is a helper function that can be used since in LSP we don't have the actual keypress events. So we don't know * (exactly) whether a position was reached through for instance inserting a new line or backspacing the next line. * * @param fileContext The file with left and right context based on the invocation position * @returns The TriggerType */ export const triggerType = (fileContext: FileContext): CodewhispererAutomatedTriggerType => { const trimmedLeftContext = fileContext.leftFileContent.trimEnd() if (isSpecialCharacter(trimmedLeftContext.at(-1) || '')) { return 'SpecialCharacters' } const lastCRLF = fileContext.leftFileContent.lastIndexOf('\r\n') if (lastCRLF >= 0 && fileContext.leftFileContent.substring(lastCRLF + 2).trim() === '') { return 'Enter' } const lastLF = fileContext.leftFileContent.lastIndexOf('\n') if (lastLF >= 0 && fileContext.leftFileContent.substring(lastLF + 1).trim() === '') { return 'Enter' } return 'Classifier' } // Normalize values based on minn and maxx values in the coefficients. const normalize = (val: number, field: keyof typeof typedCoefficients.minn & keyof typeof typedCoefficients.maxx) => (val - typedCoefficients.minn[field]) / (typedCoefficients.maxx[field] - typedCoefficients.minn[field]) /** * Parameters to the auto trigger. Contains all information to make a decision. */ type AutoTriggerParams = { fileContext: FileContext char: string triggerType: string // Left as String intentionally to support future and unknown trigger types os: string previousDecision: string ide: string lineNum: number } /** * Auto Trigger to determine whether a keystroke or edit should trigger a recommendation invocation. * It uses information about the file, the position, the last entered character, the environment, * and previous recommendation decisions from the user to determine whether a new recommendation * should be shown. The auto-trigger is not stateful and does not keep track of past invocations. */ export const autoTrigger = ({ fileContext, char, triggerType, os, previousDecision, ide, lineNum, }: AutoTriggerParams): { shouldTrigger: boolean classifierResult: number classifierThreshold: number } => { const leftContextLines = fileContext.leftFileContent.split(/\r?\n/) const leftContextAtCurrentLine = leftContextLines[leftContextLines.length - 1] const tokens = leftContextAtCurrentLine.trim().split(' ') const lastToken = tokens[tokens.length - 1] const keyword = lastToken?.length > 1 ? lastToken : '' const lengthOfLeftCurrent = leftContextLines[leftContextLines.length - 1].length const lengthOfLeftPrev = leftContextLines[leftContextLines.length - 2]?.length ?? 0 const lengthOfRight = fileContext.rightFileContent.trim().length const triggerTypeCoefficient = coefficients.triggerTypeCoefficient[triggerType] ?? 0 const osCoefficient = coefficients.osCoefficient[os] ?? 0 const charCoefficient = coefficients.charCoefficient[char] ?? 0 const keyWordCoefficient = coefficients.charCoefficient[keyword] ?? 0 const languageCoefficient = coefficients.languageCoefficient[fileContext.programmingLanguage.languageName] ?? 0 let previousDecisionCoefficient = 0 if (previousDecision === 'Accept') { previousDecisionCoefficient = coefficients.prevDecisionAcceptCoefficient } else if (previousDecision === 'Reject') { previousDecisionCoefficient = coefficients.prevDecisionRejectCoefficient } else if (previousDecision === 'Discard' || previousDecision === 'Empty') { previousDecisionCoefficient = coefficients.prevDecisionOtherCoefficient } const ideCoefficient = coefficients.ideCoefficient[ide] ?? 0 let leftContextLengthCoefficient = 0 if (fileContext.leftFileContent.length >= 0 && fileContext.leftFileContent.length < 5) { leftContextLengthCoefficient = coefficients.lengthLeft0To5Coefficient } else if (fileContext.leftFileContent.length >= 5 && fileContext.leftFileContent.length < 10) { leftContextLengthCoefficient = coefficients.lengthLeft5To10Coefficient } else if (fileContext.leftFileContent.length >= 10 && fileContext.leftFileContent.length < 20) { leftContextLengthCoefficient = coefficients.lengthLeft10To20Coefficient } else if (fileContext.leftFileContent.length >= 20 && fileContext.leftFileContent.length < 30) { leftContextLengthCoefficient = coefficients.lengthLeft20To30Coefficient } else if (fileContext.leftFileContent.length >= 30 && fileContext.leftFileContent.length < 40) { leftContextLengthCoefficient = coefficients.lengthLeft30To40Coefficient } else if (fileContext.leftFileContent.length >= 40 && fileContext.leftFileContent.length < 50) { leftContextLengthCoefficient = coefficients.lengthLeft40To50Coefficient } const classifierResult = coefficients.lengthOfRightCoefficient * normalize(lengthOfRight, 'lenRight') + coefficients.lengthOfLeftCurrentCoefficient * normalize(lengthOfLeftCurrent, 'lenLeftCur') + coefficients.lengthOfLeftPrevCoefficient * normalize(lengthOfLeftPrev, 'lenLeftPrev') + coefficients.lineNumCoefficient * normalize(lineNum, 'lineNum') + osCoefficient + triggerTypeCoefficient + charCoefficient + keyWordCoefficient + ideCoefficient + coefficients.intercept + previousDecisionCoefficient + languageCoefficient + leftContextLengthCoefficient const shouldTrigger = sigmoid(classifierResult) > TRIGGER_THRESHOLD return { shouldTrigger, classifierResult, classifierThreshold: TRIGGER_THRESHOLD, } }