in packages/core/src/codewhisperer/service/recommendationHandler.ts [167:434]
async getRecommendations(
client: DefaultCodeWhispererClient,
editor: vscode.TextEditor,
triggerType: CodewhispererTriggerType,
config: ConfigurationEntry,
autoTriggerType?: CodewhispererAutomatedTriggerType,
pagination: boolean = true,
page: number = 0,
generate: boolean = isIamConnection(AuthUtil.instance.conn)
): Promise<GetRecommendationsResponse> {
let invocationResult: 'Succeeded' | 'Failed' = 'Failed'
let errorMessage: string | undefined = undefined
let errorCode: string | undefined = undefined
if (!editor) {
return Promise.resolve<GetRecommendationsResponse>({
result: invocationResult,
errorMessage: errorMessage,
recommendationCount: 0,
})
}
let recommendations: RecommendationsList = []
let requestId = ''
let sessionId = ''
let reason = ''
let startTime = 0
let latency = 0
let nextToken = ''
let shouldRecordServiceInvocation = true
session.language = runtimeLanguageContext.getLanguageContext(
editor.document.languageId,
path.extname(editor.document.fileName)
).language
session.taskType = await this.getTaskTypeFromEditorFileName(editor.document.fileName)
if (pagination && !generate) {
if (page === 0) {
session.requestContext = await EditorContext.buildListRecommendationRequest(
editor as vscode.TextEditor,
this.nextToken,
config.isSuggestionsWithCodeReferencesEnabled
)
} else {
session.requestContext = {
request: {
...session.requestContext.request,
// Putting nextToken assignment in the end so it overwrites the existing nextToken
nextToken: this.nextToken,
},
supplementalMetadata: session.requestContext.supplementalMetadata,
}
}
} else {
session.requestContext = await EditorContext.buildGenerateRecommendationRequest(editor as vscode.TextEditor)
}
const request = session.requestContext.request
// record preprocessing end time
TelemetryHelper.instance.setPreprocessEndTime()
// set start pos for non pagination call or first pagination call
if (!pagination || (pagination && page === 0)) {
session.startPos = editor.selection.active
session.startCursorOffset = editor.document.offsetAt(session.startPos)
session.leftContextOfCurrentLine = EditorContext.getLeftContext(editor, session.startPos.line)
session.triggerType = triggerType
session.autoTriggerType = autoTriggerType
/**
* Validate request
*/
if (!EditorContext.validateRequest(request)) {
getLogger().verbose('Invalid Request: %O', request)
const languageName = request.fileContext.programmingLanguage.languageName
if (!runtimeLanguageContext.isLanguageSupported(languageName)) {
errorMessage = `${languageName} is currently not supported by Amazon Q inline suggestions`
}
return Promise.resolve<GetRecommendationsResponse>({
result: invocationResult,
errorMessage: errorMessage,
recommendationCount: 0,
})
}
}
try {
startTime = performance.now()
this.lastInvocationTime = startTime
const mappedReq = runtimeLanguageContext.mapToRuntimeLanguage(request)
const codewhispererPromise =
pagination && !generate
? client.listRecommendations(mappedReq)
: client.generateRecommendations(mappedReq)
const resp = await this.getServerResponse(triggerType, config.isManualTriggerEnabled, codewhispererPromise)
TelemetryHelper.instance.setSdkApiCallEndTime()
latency = startTime !== 0 ? performance.now() - startTime : 0
if ('recommendations' in resp) {
recommendations = (resp && resp.recommendations) || []
} else {
recommendations = (resp && resp.completions) || []
}
invocationResult = 'Succeeded'
requestId = resp?.$response && resp?.$response?.requestId
nextToken = resp?.nextToken ? resp?.nextToken : ''
sessionId = resp?.$response?.httpResponse?.headers['x-amzn-sessionid']
TelemetryHelper.instance.setFirstResponseRequestId(requestId)
if (page === 0) {
session.setTimeToFirstRecommendation(performance.now())
}
if (nextToken === '') {
TelemetryHelper.instance.setAllPaginationEndTime()
}
} catch (error) {
if (error instanceof CognitoCredentialsError) {
shouldRecordServiceInvocation = false
}
if (latency === 0) {
latency = startTime !== 0 ? performance.now() - startTime : 0
}
getLogger().error('amazonq inline-suggest: Invocation Exception : %s', (error as Error).message)
if (isAwsError(error)) {
errorMessage = error.message
requestId = error.requestId || ''
errorCode = error.code
reason = `CodeWhisperer Invocation Exception: ${error?.code ?? error?.name ?? 'unknown'}`
await this.onThrottlingException(error, triggerType)
if (error?.code === 'AccessDeniedException' && errorMessage?.includes('no identity-based policy')) {
getLogger().error('amazonq inline-suggest: AccessDeniedException : %s', (error as Error).message)
void vscode.window
.showErrorMessage(`CodeWhisperer: ${error?.message}`, CodeWhispererConstants.settingsLearnMore)
.then(async (resp) => {
if (resp === CodeWhispererConstants.settingsLearnMore) {
void openUrl(vscode.Uri.parse(CodeWhispererConstants.learnMoreUri))
}
})
await vscode.commands.executeCommand('aws.amazonq.enableCodeSuggestions', false)
}
} else {
errorMessage = error instanceof Error ? error.message : String(error)
reason = error ? String(error) : 'unknown'
}
} finally {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
let msg = indent(
`codewhisperer: request-id: ${requestId},
timestamp(epoch): ${Date.now()},
timezone: ${timezone},
datetime: ${new Date().toLocaleString([], { timeZone: timezone })},
vscode version: '${vscode.version}',
extension version: '${extensionVersion}',
filename: '${EditorContext.getFileName(editor)}',
left context of line: '${session.leftContextOfCurrentLine}',
line number: ${session.startPos.line},
character location: ${session.startPos.character},
latency: ${latency} ms.
Recommendations:`,
4,
true
).trimStart()
for (const [index, item] of recommendations.entries()) {
msg += `\n ${index.toString().padStart(2, '0')}: ${indent(item.content, 8, true).trim()}`
session.requestIdList.push(requestId)
}
getLogger().debug(msg)
if (invocationResult === 'Succeeded') {
CodeWhispererCodeCoverageTracker.getTracker(session.language)?.incrementServiceInvocationCount()
UserWrittenCodeTracker.instance.onQFeatureInvoked()
} else {
if (
(errorMessage?.includes(invalidCustomizationMessage) && errorCode === 'AccessDeniedException') ||
errorCode === 'ResourceNotFoundException'
) {
getLogger()
.debug(`The selected customization is no longer available. Retrying with the default model.
Failed request id: ${requestId}`)
await switchToBaseCustomizationAndNotify()
await this.getRecommendations(
client,
editor,
triggerType,
config,
autoTriggerType,
pagination,
page,
true
)
}
}
if (shouldRecordServiceInvocation) {
TelemetryHelper.instance.recordServiceInvocationTelemetry(
requestId,
sessionId,
session.recommendations.length + recommendations.length - 1,
invocationResult,
latency,
session.language,
session.taskType,
reason,
session.requestContext.supplementalMetadata
)
}
}
if (this.isCancellationRequested()) {
return Promise.resolve<GetRecommendationsResponse>({
result: invocationResult,
errorMessage: errorMessage,
recommendationCount: session.recommendations.length,
})
}
const typedPrefix = editor.document
.getText(new vscode.Range(session.startPos, editor.selection.active))
.replace('\r\n', '\n')
if (recommendations.length > 0) {
TelemetryHelper.instance.setTypeAheadLength(typedPrefix.length)
// mark suggestions that does not match typeahead when arrival as Discard
// these suggestions can be marked as Showed if typeahead can be removed with new inline API
for (const [i, r] of recommendations.entries()) {
const recommendationIndex = i + session.recommendations.length
if (
!r.content.startsWith(typedPrefix) &&
session.getSuggestionState(recommendationIndex) === undefined
) {
session.setSuggestionState(recommendationIndex, 'Discard')
}
session.setCompletionType(recommendationIndex, r)
}
session.recommendations = pagination ? session.recommendations.concat(recommendations) : recommendations
if (isInlineCompletionEnabled() && this.hasAtLeastOneValidSuggestion(typedPrefix)) {
this._onDidReceiveRecommendation.fire()
}
}
this.requestId = requestId
session.sessionId = sessionId
this.nextToken = nextToken
// send Empty userDecision event if user receives no recommendations in this session at all.
if (invocationResult === 'Succeeded' && nextToken === '') {
// case 1: empty list of suggestion []
if (session.recommendations.length === 0) {
session.requestIdList.push(requestId)
// Received an empty list of recommendations
TelemetryHelper.instance.recordUserDecisionTelemetryForEmptyList(
session.requestIdList,
sessionId,
page,
runtimeLanguageContext.getLanguageContext(
editor.document.languageId,
path.extname(editor.document.fileName)
).language,
session.requestContext.supplementalMetadata
)
}
// case 2: non empty list of suggestion but with (a) empty content or (b) non-matching typeahead
else if (!this.hasAtLeastOneValidSuggestion(typedPrefix)) {
this.reportUserDecisions(-1)
}
}
return Promise.resolve<GetRecommendationsResponse>({
result: invocationResult,
errorMessage: errorMessage,
recommendationCount: session.recommendations.length,
})
}