Skip to content

feat(amazonq): parse new transformation plan #7340

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions packages/core/src/codewhisperer/commands/startTransformByQ.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
downloadHilResultArchive,
findDownloadArtifactStep,
getArtifactsFromProgressUpdate,
getTransformationPlan,
getTransformationSteps,
pollTransformationJob,
resumeTransformationJob,
Expand Down Expand Up @@ -554,28 +553,6 @@ export async function pollTransformationStatusUntilPlanReady(jobId: string, prof
// for now, no plan shown with SQL conversions. later, we may add one
return
}
let plan = undefined
try {
plan = await getTransformationPlan(jobId, profile)
} catch (error) {
// means API call failed
getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error)
transformByQState.setJobFailureErrorNotification(
`${CodeWhispererConstants.failedToGetPlanNotification} ${(error as Error).message}`
)
transformByQState.setJobFailureErrorChatMessage(
`${CodeWhispererConstants.failedToGetPlanChatMessage} ${(error as Error).message}`
)
throw new Error('Get plan failed')
}

if (plan !== undefined) {
const planFilePath = path.join(transformByQState.getProjectPath(), 'transformation-plan.md')
fs.writeFileSync(planFilePath, plan)
await vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(planFilePath))
transformByQState.setPlanFilePath(planFilePath)
await setContext('gumby.isPlanAvailable', true)
}
jobPlanProgress['generatePlan'] = StepProgress.Succeeded
throwIfCancelled()
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/codewhisperer/models/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@ export class ZipManifest {
version: string = '1.0'
hilCapabilities: string[] = ['HIL_1pDependency_VersionUpgrade']
// TO-DO: add 'CLIENT_SIDE_BUILD' here when releasing
// TO-DO: add something like AGENTIC_PLAN_V1 here when BE allowlists everyone
transformCapabilities: string[] = ['EXPLAINABILITY_V1']
customBuildCommand: string = 'clean test'
requestedConversions?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { encodeHTML } from '../../../shared/utilities/textUtilities'
import { convertToTimeString } from '../../../shared/datetime'
import { getAuthType } from '../../../auth/utils'
import { UserWrittenCodeTracker } from '../../tracker/userWrittenCodeTracker'
import { setContext } from '../../../shared/vscode/setContext'
import { AuthUtil } from '../../util/authUtil'
import { DiffModel } from './transformationResultsViewProvider'
import { spawnSync } from 'child_process' // eslint-disable-line no-restricted-imports
Expand Down Expand Up @@ -521,20 +522,33 @@ export function getFormattedString(s: string) {
return CodeWhispererConstants.formattedStringMap.get(s) ?? s
}

export function addTableMarkdown(plan: string, stepId: string, tableMapping: { [key: string]: string }) {
const tableObj = tableMapping[stepId]
if (!tableObj) {
// no table present for this step
export function addTableMarkdown(plan: string, stepId: string, tableMapping: { [key: string]: string[] }) {
const tableObjects = tableMapping[stepId]
if (!tableObjects || tableObjects.length === 0 || tableObjects.every((table: string) => table === '')) {
// no tables for this stepId
return plan
}
const table = JSON.parse(tableObj)
if (table.rows.length === 0) {
// empty table
plan += `\n\nThere are no ${table.name.toLowerCase()} to display.\n\n`
const tables: any[] = []
// eslint-disable-next-line unicorn/no-array-for-each
tableObjects.forEach((tableObj: string) => {
try {
const table = JSON.parse(tableObj)
if (table) {
tables.push(table)
}
} catch (e) {
getLogger().error(`CodeTransformation: Failed to parse table JSON, skipping: ${e}`)
}
})

if (tables.every((table: any) => table.rows.length === 0)) {
// empty tables for this stepId
plan += `\n\nThere are no ${tables[0].name.toLowerCase()} to display.\n\n`
return plan
}
plan += `\n\n\n${table.name}\n|`
const columns = table.columnNames
// table name and columns are shared, so only add to plan once
plan += `\n\n\n${tables[0].name}\n|`
const columns = tables[0].columnNames
// eslint-disable-next-line unicorn/no-array-for-each
columns.forEach((columnName: string) => {
plan += ` ${getFormattedString(columnName)} |`
Expand All @@ -544,28 +558,35 @@ export function addTableMarkdown(plan: string, stepId: string, tableMapping: { [
columns.forEach((_: any) => {
plan += '-----|'
})
// add all rows of all tables
// eslint-disable-next-line unicorn/no-array-for-each
table.rows.forEach((row: any) => {
plan += '\n|'
tables.forEach((table: any) => {
// eslint-disable-next-line unicorn/no-array-for-each
columns.forEach((columnName: string) => {
if (columnName === 'relativePath') {
plan += ` [${row[columnName]}](${row[columnName]}) |` // add MD link only for files
} else {
plan += ` ${row[columnName]} |`
}
table.rows.forEach((row: any) => {
plan += '\n|'
// eslint-disable-next-line unicorn/no-array-for-each
columns.forEach((columnName: string) => {
if (columnName === 'relativePath') {
// add markdown link only for file paths
plan += ` [${row[columnName]}](${row[columnName]}) |`
} else {
plan += ` ${row[columnName]} |`
}
})
})
})
plan += '\n\n'
return plan
}

export function getTableMapping(stepZeroProgressUpdates: ProgressUpdates) {
const map: { [key: string]: string } = {}
const map: { [key: string]: string[] } = {}
for (const update of stepZeroProgressUpdates) {
// description should never be undefined since even if no data we show an empty table
// but just in case, empty string allows us to skip this table without errors when rendering
map[update.name] = update.description ?? ''
if (!map[update.name]) {
map[update.name] = []
}
// empty string allows us to skip this table when rendering
map[update.name].push(update.description ?? '')
}
return map
}
Expand Down Expand Up @@ -604,7 +625,7 @@ export async function getTransformationPlan(jobId: string, profile: RegionProfil
// gets a mapping between the ID ('name' field) of each progressUpdate (substep) and the associated table
const tableMapping = getTableMapping(stepZeroProgressUpdates)

const jobStatistics = JSON.parse(tableMapping['0']).rows // ID of '0' reserved for job statistics table
const jobStatistics = JSON.parse(tableMapping['0'][0]).rows // ID of '0' reserved for job statistics table; only 1 table there

// get logo directly since we only use one logo regardless of color theme
const logoIcon = getTransformationIcon('transformLogo')
Expand All @@ -631,7 +652,7 @@ export async function getTransformationPlan(jobId: string, profile: RegionProfil
}
plan += `</div><br>`
plan += `<p style="font-size: 18px; margin-bottom: 4px;"><b>Appendix</b><br><a href="#top" style="float: right; font-size: 14px;">Scroll to top <img src="${arrowIcon}" style="vertical-align: middle;"></a></p><br>`
plan = addTableMarkdown(plan, '-1', tableMapping) // ID of '-1' reserved for appendix table
plan = addTableMarkdown(plan, '-1', tableMapping) // ID of '-1' reserved for appendix table; only 1 table there
return plan
} catch (e: any) {
const errorMessage = (e as Error).message
Expand Down Expand Up @@ -663,6 +684,7 @@ export async function getTransformationSteps(jobId: string, profile: RegionProfi

export async function pollTransformationJob(jobId: string, validStates: string[], profile: RegionProfile | undefined) {
let status: string = ''
let isPlanComplete = false
while (true) {
throwIfCancelled()
try {
Expand Down Expand Up @@ -699,6 +721,19 @@ export async function pollTransformationJob(jobId: string, validStates: string[]
`${CodeWhispererConstants.failedToCompleteJobGenericNotification} ${errorMessage}`
)
}

if (
CodeWhispererConstants.validStatesForPlanGenerated.includes(status) &&
transformByQState.getTransformationType() === TransformationType.LANGUAGE_UPGRADE &&
!isPlanComplete
) {
const plan = await openTransformationPlan(jobId, profile)
if (plan?.toLowerCase().includes('dependency changes')) {
// final plan is complete; show to user
isPlanComplete = true
}
}

if (validStates.includes(status)) {
break
}
Expand Down Expand Up @@ -738,6 +773,32 @@ export async function pollTransformationJob(jobId: string, validStates: string[]
return status
}

async function openTransformationPlan(jobId: string, profile?: RegionProfile) {
let plan = undefined
try {
plan = await getTransformationPlan(jobId, profile)
} catch (error) {
// means API call failed
getLogger().error(`CodeTransformation: ${CodeWhispererConstants.failedToCompleteJobNotification}`, error)
transformByQState.setJobFailureErrorNotification(
`${CodeWhispererConstants.failedToGetPlanNotification} ${(error as Error).message}`
)
transformByQState.setJobFailureErrorChatMessage(
`${CodeWhispererConstants.failedToGetPlanChatMessage} ${(error as Error).message}`
)
throw new Error('Get plan failed')
}

if (plan) {
const planFilePath = path.join(transformByQState.getProjectPath(), 'transformation-plan.md')
nodefs.writeFileSync(planFilePath, plan)
await vscode.commands.executeCommand('markdown.showPreview', vscode.Uri.file(planFilePath))
transformByQState.setPlanFilePath(planFilePath)
await setContext('gumby.isPlanAvailable', true)
}
return plan
}

async function attemptLocalBuild() {
const jobId = transformByQState.getJobId()
let artifactId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,25 @@ dependencyManagement:
},
transformationJob: { status: 'COMPLETED' },
}
const mockPlanResponse = {
$response: {
data: {
transformationPlan: { transformationSteps: [] },
},
requestId: 'requestId',
hasNextPage: () => false,
error: undefined,
nextPage: () => null, // eslint-disable-line unicorn/no-null
redirectCount: 0,
retryCount: 0,
httpResponse: new HttpResponse(),
},
transformationPlan: { transformationSteps: [] },
}
sinon.stub(codeWhisperer.codeWhispererClient, 'codeModernizerGetCodeTransformation').resolves(mockJobResponse)
sinon
.stub(codeWhisperer.codeWhispererClient, 'codeModernizerGetCodeTransformationPlan')
.resolves(mockPlanResponse)
transformByQState.setToSucceeded()
const status = await pollTransformationJob(
'dummyId',
Expand Down Expand Up @@ -488,12 +506,18 @@ dependencyManagement:

const actual = getTableMapping(stepZeroProgressUpdates)
const expected = {
'0': '{"columnNames":["name","value"],"rows":[{"name":"Lines of code in your application","value":"3000"},{"name":"Dependencies to be replaced","value":"5"},{"name":"Deprecated code instances to be replaced","value":"10"},{"name":"Files to be updated","value":"7"}]}',
'1-dependency-change-abc':
'0': [
'{"columnNames":["name","value"],"rows":[{"name":"Lines of code in your application","value":"3000"},{"name":"Dependencies to be replaced","value":"5"},{"name":"Deprecated code instances to be replaced","value":"10"},{"name":"Files to be updated","value":"7"}]}',
],
'1-dependency-change-abc': [
'{"columnNames":["dependencyName","action","currentVersion","targetVersion"],"rows":[{"dependencyName":"org.springboot.com","action":"Update","currentVersion":"2.1","targetVersion":"2.4"}, {"dependencyName":"com.lombok.java","action":"Remove","currentVersion":"1.7","targetVersion":"-"}]}',
'2-deprecated-code-xyz':
],
'2-deprecated-code-xyz': [
'{"columnNames":["apiFullyQualifiedName","numChangedFiles"],“rows”:[{"apiFullyQualifiedName":"java.lang.Thread.stop()","numChangedFiles":"6"}, {"apiFullyQualifiedName":"java.math.bad()","numChangedFiles":"3"}]}',
'-1': '{"columnNames":["relativePath","action"],"rows":[{"relativePath":"pom.xml","action":"Update"}, {"relativePath":"src/main/java/com/bhoruka/bloodbank/BloodbankApplication.java","action":"Update"}]}',
],
'-1': [
'{"columnNames":["relativePath","action"],"rows":[{"relativePath":"pom.xml","action":"Update"}, {"relativePath":"src/main/java/com/bhoruka/bloodbank/BloodbankApplication.java","action":"Update"}]}',
],
}
assert.deepStrictEqual(actual, expected)
})
Expand Down
Loading