Skip to content

Commit 0da289c

Browse files
authored
Amazon Q Transform Feature - human in the loop (#4920)
Allow users to interactively update dependencies while transformation is paused waiting for user input.
1 parent b0b50d2 commit 0da289c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2388
-232
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Transform: Add human intervention to help update dependencies during transformation."
4+
}

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3979,7 +3979,7 @@
39793979
},
39803980
"devDependencies": {
39813981
"@aws-sdk/types": "^3.13.1",
3982-
"@aws-toolkits/telemetry": "^1.0.203",
3982+
"@aws-toolkits/telemetry": "^1.0.205",
39833983
"@aws/fully-qualified-names": "^2.1.4",
39843984
"@cspotcode/source-map-support": "^0.8.1",
39853985
"@sinonjs/fake-timers": "^10.0.2",

packages/core/scripts/build/copyFiles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ interface CopyTask {
2727
const tasks: CopyTask[] = [
2828
{ target: path.join('src', 'templates') },
2929
{ target: path.join('src', 'test', 'shared', 'cloudformation', 'yaml') },
30-
{ target: path.join('src', 'test', 'codewhisperer', 'service', 'resources') },
30+
{ target: path.join('src', 'test', 'amazonqGumby', 'resources') },
3131
{ target: path.join('src', 'testFixtures') },
3232
{ target: 'src/auth/sso/vue' },
3333

packages/core/src/amazonqFeatureDev/models.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,11 @@ export type ChatItemType =
1313
| 'answer-stream'
1414
| 'answer-part'
1515
| 'code-result'
16+
17+
export interface IManifestFile {
18+
pomArtifactId: string
19+
pomFolderName: string
20+
hilCapability: string
21+
pomGroupId: string
22+
sourcePomVersion: string
23+
}

packages/core/src/amazonqGumby/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export function init(appContext: AmazonQAppInitContext) {
2727
transformationFinished: new vscode.EventEmitter<any>(),
2828
processHumanChatMessage: new vscode.EventEmitter<any>(),
2929
linkClicked: new vscode.EventEmitter<any>(),
30+
humanInTheLoopStartIntervention: new vscode.EventEmitter<any>(),
31+
humanInTheLoopPromptUserForDependency: new vscode.EventEmitter<any>(),
32+
humanInTheLoopSelectionUploaded: new vscode.EventEmitter<any>(),
33+
errorThrown: new vscode.EventEmitter<any>(),
3034
}
3135

3236
const dispatcher = new AppToWebViewMessageDispatcher(appContext.getAppsToWebViewMessagePublisher())

packages/core/src/amazonqGumby/chat/controller/controller.ts

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,31 @@ import { featureName } from '../../models/constants'
1717
import { AuthUtil } from '../../../codewhisperer/util/authUtil'
1818
import {
1919
compileProject,
20+
finishHumanInTheLoop,
2021
getValidCandidateProjects,
22+
openHilPomFile,
2123
processTransformFormInput,
2224
startTransformByQ,
2325
stopTransformByQ,
2426
validateCanCompileProject,
2527
} from '../../../codewhisperer/commands/startTransformByQ'
2628
import { JDKVersion, TransformationCandidateProject, transformByQState } from '../../../codewhisperer/models/model'
29+
import {
30+
AlternateDependencyVersionsNotFoundError,
31+
JavaHomeNotSetError,
32+
ModuleUploadError,
33+
NoJavaProjectsFoundError,
34+
NoMavenJavaProjectsFoundError,
35+
} from '../../errors'
2736
import * as CodeWhispererConstants from '../../../codewhisperer/models/constants'
28-
import { JavaHomeNotSetError, NoJavaProjectsFoundError, NoMavenJavaProjectsFoundError } from '../../errors'
2937
import MessengerUtils, { ButtonActions, GumbyCommands } from './messenger/messengerUtils'
3038
import { CancelActionPositions } from '../../telemetry/codeTransformTelemetry'
3139
import { openUrl } from '../../../shared/utilities/vsCodeUtils'
3240
import { telemetry } from '../../../shared/telemetry/telemetry'
3341
import { MetadataResult } from '../../../shared/telemetry/telemetryClient'
3442
import { CodeTransformTelemetryState } from '../../telemetry/codeTransformTelemetryState'
3543
import { getAuthType } from '../../../codewhisperer/service/transformByQ/transformApiHandler'
44+
import DependencyVersions from '../../models/dependencies'
3645

3746
// These events can be interactions within the chat,
3847
// or elsewhere in the IDE
@@ -46,6 +55,10 @@ export interface ChatControllerEventEmitters {
4655
readonly transformationFinished: vscode.EventEmitter<any>
4756
readonly processHumanChatMessage: vscode.EventEmitter<any>
4857
readonly linkClicked: vscode.EventEmitter<any>
58+
readonly humanInTheLoopStartIntervention: vscode.EventEmitter<any>
59+
readonly humanInTheLoopPromptUserForDependency: vscode.EventEmitter<any>
60+
readonly humanInTheLoopSelectionUploaded: vscode.EventEmitter<any>
61+
readonly errorThrown: vscode.EventEmitter<any>
4962
}
5063

5164
export class GumbyController {
@@ -97,6 +110,22 @@ export class GumbyController {
97110
this.chatControllerMessageListeners.linkClicked.event(data => {
98111
this.openLink(data)
99112
})
113+
114+
this.chatControllerMessageListeners.humanInTheLoopStartIntervention.event(data => {
115+
return this.startHILIntervention(data)
116+
})
117+
118+
this.chatControllerMessageListeners.humanInTheLoopPromptUserForDependency.event(data => {
119+
return this.HILPromptForDependency(data)
120+
})
121+
122+
this.chatControllerMessageListeners.humanInTheLoopSelectionUploaded.event(data => {
123+
return this.HILDependencySelectionUploaded(data)
124+
})
125+
126+
this.chatControllerMessageListeners.errorThrown.event(data => {
127+
return this.handleError(data)
128+
})
100129
}
101130

102131
private async tabOpened(message: any) {
@@ -143,7 +172,7 @@ export class GumbyController {
143172
// check that a project is open
144173
const workspaceFolders = vscode.workspace.workspaceFolders
145174
if (workspaceFolders === undefined || workspaceFolders.length === 0) {
146-
this.messenger.sendRetryableErrorResponse('no-project-found', message.tabID)
175+
this.messenger.sendUnrecoverableErrorResponse('no-project-found', message.tabID)
147176
return
148177
}
149178

@@ -197,11 +226,11 @@ export class GumbyController {
197226
return await getValidCandidateProjects()
198227
} catch (err: any) {
199228
if (err instanceof NoJavaProjectsFoundError) {
200-
this.messenger.sendRetryableErrorResponse('no-java-project-found', message.tabID)
229+
this.messenger.sendUnrecoverableErrorResponse('no-java-project-found', message.tabID)
201230
} else if (err instanceof NoMavenJavaProjectsFoundError) {
202-
this.messenger.sendRetryableErrorResponse('no-maven-java-project-found', message.tabID)
231+
this.messenger.sendUnrecoverableErrorResponse('no-maven-java-project-found', message.tabID)
203232
} else {
204-
this.messenger.sendRetryableErrorResponse('no-project-found', message.tabID)
233+
this.messenger.sendUnrecoverableErrorResponse('no-project-found', message.tabID)
205234
}
206235
}
207236
return []
@@ -227,6 +256,16 @@ export class GumbyController {
227256
this.messenger.sendCommandMessage({ ...message, command: GumbyCommands.CLEAR_CHAT })
228257
await this.transformInitiated(message)
229258
break
259+
case ButtonActions.CONFIRM_DEPENDENCY_FORM:
260+
await this.continueJobWithSelectedDependency({ ...message, tabID: message.tabId })
261+
break
262+
case ButtonActions.CANCEL_DEPENDENCY_FORM:
263+
this.messenger.sendUserPrompt('Cancel', message.tabId)
264+
await this.continueTransformationWithoutHIL({ tabID: message.tabId })
265+
break
266+
case ButtonActions.OPEN_FILE:
267+
await openHilPomFile()
268+
break
230269
}
231270
}
232271

@@ -246,7 +285,7 @@ export class GumbyController {
246285
this.messenger.sendProjectSelectionMessage(projectName, fromJDKVersion, toJDKVersion, message.tabID)
247286

248287
if (fromJDKVersion === JDKVersion.UNSUPPORTED) {
249-
this.messenger.sendRetryableErrorResponse('unsupported-source-jdk-version', message.tabID)
288+
this.messenger.sendUnrecoverableErrorResponse('unsupported-source-jdk-version', message.tabID)
250289
return
251290
}
252291

@@ -267,7 +306,7 @@ export class GumbyController {
267306
this.messenger.sendCompilationInProgress(message.tabID)
268307
await compileProject()
269308
} catch (err: any) {
270-
this.messenger.sendRetryableErrorResponse('could-not-compile-project', message.tabID)
309+
this.messenger.sendUnrecoverableErrorResponse('could-not-compile-project', message.tabID)
271310
// reset state to allow "Start a new transformation" button to work
272311
this.sessionStorage.getSession().conversationState = ConversationState.IDLE
273312
throw err
@@ -310,12 +349,26 @@ export class GumbyController {
310349
await this.prepareProjectForSubmission(message)
311350
}
312351

313-
private async transformationFinished(data: { message: string; tabID: string }) {
352+
private async transformationFinished(data: { message?: string; tabID: string }) {
314353
this.sessionStorage.getSession().conversationState = ConversationState.IDLE
315354
// at this point job is either completed, partially_completed, cancelled, or failed
316355
this.messenger.sendJobFinishedMessage(data.tabID, data.message)
317356
}
318357

358+
private startHILIntervention(data: { tabID: string; codeSnippet: string }) {
359+
this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT
360+
this.messenger.sendHumanInTheLoopInitialMessage(data.tabID, data.codeSnippet)
361+
}
362+
363+
private HILPromptForDependency(data: { tabID: string; dependencies: DependencyVersions }) {
364+
this.messenger.sendDependencyVersionsFoundMessage(data.dependencies, data.tabID)
365+
}
366+
367+
private HILDependencySelectionUploaded(data: { tabID: string }) {
368+
this.sessionStorage.getSession().conversationState = ConversationState.JOB_SUBMITTED
369+
this.messenger.sendHILResumeMessage(data.tabID)
370+
}
371+
319372
private async processHumanChatMessage(data: { message: string; tabID: string }) {
320373
this.messenger.sendUserPrompt(data.message, data.tabID)
321374
this.messenger.sendChatInputEnabled(data.tabID, false)
@@ -332,15 +385,47 @@ export class GumbyController {
332385
tabID: data.tabID,
333386
})
334387
} else {
335-
this.messenger.sendRetryableErrorResponse('invalid-java-home', data.tabID)
388+
this.messenger.sendUnrecoverableErrorResponse('invalid-java-home', data.tabID)
336389
}
337390
}
338391
}
339392
}
340393

394+
private async continueJobWithSelectedDependency(message: { tabID: string; formSelectedValues: any }) {
395+
const selectedDependency = message.formSelectedValues['GumbyTransformDependencyForm']
396+
this.messenger.sendHILContinueMessage(message.tabID, selectedDependency)
397+
await finishHumanInTheLoop(selectedDependency)
398+
}
399+
341400
private openLink(message: { link: string }) {
342401
void openUrl(vscode.Uri.parse(message.link))
343402
}
403+
404+
private async handleError(message: { error: Error; tabID: string }) {
405+
if (message.error instanceof AlternateDependencyVersionsNotFoundError) {
406+
this.messenger.sendKnownErrorResponse('no-alternate-dependencies-found', message.tabID)
407+
await this.continueTransformationWithoutHIL(message)
408+
} else if (message.error instanceof ModuleUploadError) {
409+
this.messenger.sendKnownErrorResponse('upload-to-s3-failed', message.tabID)
410+
await this.transformationFinished(message)
411+
} else {
412+
this.messenger.sendErrorMessage(message.error.message, message.tabID)
413+
}
414+
}
415+
416+
private async continueTransformationWithoutHIL(message: { tabID: string }) {
417+
this.sessionStorage.getSession().conversationState = ConversationState.JOB_SUBMITTED
418+
CodeTransformTelemetryState.instance.setCodeTransformMetaDataField({
419+
canceledFromChat: true,
420+
})
421+
try {
422+
await finishHumanInTheLoop()
423+
} catch (err: any) {
424+
await this.transformationFinished({ tabID: message.tabID })
425+
}
426+
427+
this.messenger.sendStaticTextResponse('end-HIL-early', message.tabID)
428+
}
344429
}
345430

346431
/**

0 commit comments

Comments
 (0)