Skip to content

Commit 592e5c7

Browse files
committed
POC to test the streaming API from Q Chat
1 parent 643ae10 commit 592e5c7

File tree

4 files changed

+464
-113
lines changed

4 files changed

+464
-113
lines changed

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

Lines changed: 159 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import { randomUUID } from '../../../shared/crypto'
5353
import { tempDirPath, testGenerationLogsDir } from '../../../shared/filesystemUtilities'
5454
import { CodeReference } from '../../../codewhispererChat/view/connector/connector'
5555
import { TelemetryHelper } from '../../../codewhisperer/util/telemetryHelper'
56+
import { QCliStreamingService } from '../streaming/qCliStreamingService'
5657
import { Reference, testGenState } from '../../../codewhisperer/models/model'
5758
import {
5859
referenceLogText,
@@ -111,6 +112,9 @@ export class TestController {
111112
this.authController = new AuthController()
112113
this.editorContentController = new EditorContentController()
113114

115+
// Initialize the Q CLI streaming service
116+
QCliStreamingService.getInstance().initialize(messenger)
117+
114118
this.chatControllerMessageListeners.tabOpened.event((data) => {
115119
return this.tabOpened(data)
116120
})
@@ -218,6 +222,12 @@ export class TestController {
218222
session.isAuthenticating = true
219223
return
220224
}
225+
226+
// Update placeholder to indicate Q CLI-like functionality
227+
this.messenger.sendUpdatePlaceholder(
228+
tabID,
229+
'Ask Amazon Q a question or use /test to generate unit tests...'
230+
)
221231
} catch (err: any) {
222232
logger.error('tabOpened failed: %O', err)
223233
this.messenger.sendErrorMessage(err.message, message.tabID)
@@ -413,8 +423,12 @@ export class TestController {
413423
this.messenger.sendMessage(`Updated command to \`${updatedCommands}\``, data.tabID, 'prompt')
414424
await this.checkForInstallationDependencies(data)
415425
return
416-
} else {
426+
} else if (data.prompt.startsWith('/test')) {
427+
// If the prompt starts with /test, use the original test generation flow
417428
await this.startTestGen(data, false)
429+
} else {
430+
// Otherwise, use the Q CLI streaming functionality
431+
await this.processQCliStreamingMessage(data)
418432
}
419433
}
420434
// This function takes filePath as input parameter and returns file language
@@ -474,124 +488,127 @@ export class TestController {
474488
)
475489
return
476490
}
477-
// Truncating the user prompt if the prompt is more than 4096.
478-
userPrompt = message.prompt.slice(0, maxUserPromptLength)
479-
480-
// check that the session is authenticated
481-
const authState = await AuthUtil.instance.getChatAuthState()
482-
if (authState.amazonQ !== 'connected') {
483-
void this.messenger.sendAuthNeededExceptionMessage(authState, tabID)
484-
session.isAuthenticating = true
485-
return
486-
}
491+
await this.processQCliStreamingMessage(message)
492+
if (session.listOfTestGenerationJobId.length > 1) {
493+
// Truncating the user prompt if the prompt is more than 4096.
494+
userPrompt = message.prompt.slice(0, maxUserPromptLength)
495+
496+
// check that the session is authenticated
497+
const authState = await AuthUtil.instance.getChatAuthState()
498+
if (authState.amazonQ !== 'connected') {
499+
void this.messenger.sendAuthNeededExceptionMessage(authState, tabID)
500+
session.isAuthenticating = true
501+
return
502+
}
487503

488-
// check that a project/workspace is open
489-
const workspaceFolders = vscode.workspace.workspaceFolders
490-
if (workspaceFolders === undefined || workspaceFolders.length === 0) {
491-
this.messenger.sendUnrecoverableErrorResponse('no-project-found', tabID)
492-
return
493-
}
504+
// check that a project/workspace is open
505+
const workspaceFolders = vscode.workspace.workspaceFolders
506+
if (workspaceFolders === undefined || workspaceFolders.length === 0) {
507+
this.messenger.sendUnrecoverableErrorResponse('no-project-found', tabID)
508+
return
509+
}
494510

495-
// check if IDE has active file open.
496-
const activeEditor = vscode.window.activeTextEditor
497-
// also check all open editors and allow this to proceed if only one is open (even if not main focus)
498-
const allVisibleEditors = vscode.window.visibleTextEditors
499-
const openFileEditors = allVisibleEditors.filter((editor) => editor.document.uri.scheme === 'file')
500-
const hasOnlyOneOpenFileSplitView = openFileEditors.length === 1
501-
getLogger().debug(`hasOnlyOneOpenSplitView: ${hasOnlyOneOpenFileSplitView}`)
502-
// is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open
503-
const isNotFile = activeEditor?.document.uri.scheme !== 'file' && !hasOnlyOneOpenFileSplitView
504-
getLogger().debug(`activeEditor: ${activeEditor}, isNotFile: ${isNotFile}`)
505-
if (!activeEditor || isNotFile) {
506-
this.messenger.sendUnrecoverableErrorResponse(
507-
isNotFile ? 'invalid-file-type' : 'no-open-file-found',
508-
tabID
509-
)
510-
this.messenger.sendUpdatePlaceholder(
511-
tabID,
512-
'Please open and highlight a source code file in order to generate tests.'
513-
)
514-
this.messenger.sendChatInputEnabled(tabID, true)
515-
this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT
516-
return
517-
}
511+
// check if IDE has active file open.
512+
const activeEditor = vscode.window.activeTextEditor
513+
// also check all open editors and allow this to proceed if only one is open (even if not main focus)
514+
const allVisibleEditors = vscode.window.visibleTextEditors
515+
const openFileEditors = allVisibleEditors.filter((editor) => editor.document.uri.scheme === 'file')
516+
const hasOnlyOneOpenFileSplitView = openFileEditors.length === 1
517+
getLogger().debug(`hasOnlyOneOpenSplitView: ${hasOnlyOneOpenFileSplitView}`)
518+
// is not a file if the currently highlighted window is not a file, and there is either more than one or no file windows open
519+
const isNotFile = activeEditor?.document.uri.scheme !== 'file' && !hasOnlyOneOpenFileSplitView
520+
getLogger().debug(`activeEditor: ${activeEditor}, isNotFile: ${isNotFile}`)
521+
if (!activeEditor || isNotFile) {
522+
this.messenger.sendUnrecoverableErrorResponse(
523+
isNotFile ? 'invalid-file-type' : 'no-open-file-found',
524+
tabID
525+
)
526+
this.messenger.sendUpdatePlaceholder(
527+
tabID,
528+
'Please open and highlight a source code file in order to generate tests.'
529+
)
530+
this.messenger.sendChatInputEnabled(tabID, true)
531+
this.sessionStorage.getSession().conversationState = ConversationState.WAITING_FOR_INPUT
532+
return
533+
}
518534

519-
const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors[0] : activeEditor
520-
getLogger().debug(`File path: ${fileEditorToTest.document.uri.fsPath}`)
521-
filePath = fileEditorToTest.document.uri.fsPath
522-
fileName = path.basename(filePath)
523-
userFacingMessage = userPrompt
524-
? regenerateTests
525-
? `${userPrompt}`
526-
: `/test ${userPrompt}`
527-
: `/test Generate unit tests for \`${fileName}\``
528-
529-
session.hasUserPromptSupplied = userPrompt.length > 0
530-
531-
// displaying user message prompt in Test tab
532-
this.messenger.sendMessage(userFacingMessage, tabID, 'prompt')
533-
this.messenger.sendChatInputEnabled(tabID, false)
534-
this.sessionStorage.getSession().conversationState = ConversationState.IN_PROGRESS
535-
this.messenger.sendUpdatePromptProgress(message.tabID, testGenProgressField)
536-
537-
const language = await this.getLanguageForFilePath(filePath)
538-
session.fileLanguage = language
539-
const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileEditorToTest.document.uri)
540-
541-
/*
535+
const fileEditorToTest = hasOnlyOneOpenFileSplitView ? openFileEditors[0] : activeEditor
536+
getLogger().debug(`File path: ${fileEditorToTest.document.uri.fsPath}`)
537+
filePath = fileEditorToTest.document.uri.fsPath
538+
fileName = path.basename(filePath)
539+
userFacingMessage = userPrompt
540+
? regenerateTests
541+
? `${userPrompt}`
542+
: `/test ${userPrompt}`
543+
: `/test Generate unit tests for \`${fileName}\``
544+
545+
session.hasUserPromptSupplied = userPrompt.length > 0
546+
547+
// displaying user message prompt in Test tab
548+
this.messenger.sendMessage(userFacingMessage, tabID, 'prompt')
549+
this.messenger.sendChatInputEnabled(tabID, false)
550+
this.sessionStorage.getSession().conversationState = ConversationState.IN_PROGRESS
551+
this.messenger.sendUpdatePromptProgress(message.tabID, testGenProgressField)
552+
553+
const language = await this.getLanguageForFilePath(filePath)
554+
session.fileLanguage = language
555+
const workspaceFolder = vscode.workspace.getWorkspaceFolder(fileEditorToTest.document.uri)
556+
557+
/*
542558
For Re:Invent 2024 we are supporting only java and python for unit test generation, rest of the languages shows the similar experience as CWC
543559
*/
544-
if (!['java', 'python'].includes(language) || workspaceFolder === undefined) {
545-
let unsupportedMessage: string
546-
const unsupportedLanguage = language ? language.charAt(0).toUpperCase() + language.slice(1) : ''
547-
if (!workspaceFolder) {
548-
// File is outside of workspace
549-
unsupportedMessage = `<span style="color: #EE9D28;">&#9888;<b>I can't generate tests for ${fileName}</b> because the file is outside of workspace scope.<br></span> I can still provide examples, instructions and code suggestions.`
550-
} else if (unsupportedLanguage) {
551-
unsupportedMessage = `<span style="color: #EE9D28;">&#9888;<b>I'm sorry, but /test only supports Python and Java</b><br></span> While ${unsupportedLanguage} is not supported, I will generate a suggestion below.`
552-
} else {
553-
unsupportedMessage = `<span style="color: #EE9D28;">&#9888;<b>I'm sorry, but /test only supports Python and Java</b><br></span> I will still generate a suggestion below.`
554-
}
555-
this.messenger.sendMessage(unsupportedMessage, tabID, 'answer')
556-
session.isSupportedLanguage = false
557-
await this.onCodeGeneration(
558-
session,
559-
userPrompt,
560-
tabID,
561-
fileName,
562-
filePath,
563-
workspaceFolder !== undefined
564-
)
565-
} else {
566-
this.messenger.sendCapabilityCard({ tabID })
567-
this.messenger.sendMessage(testGenSummaryMessage(fileName), message.tabID, 'answer-part')
568-
569-
// Grab the selection from the fileEditorToTest and get the vscode Range
570-
const selection = fileEditorToTest.selection
571-
let selectionRange = undefined
572-
if (
573-
selection.start.line !== selection.end.line ||
574-
selection.start.character !== selection.end.character
575-
) {
576-
selectionRange = new vscode.Range(
577-
selection.start.line,
578-
selection.start.character,
579-
selection.end.line,
580-
selection.end.character
560+
if (!['java', 'python'].includes(language) || workspaceFolder === undefined) {
561+
let unsupportedMessage: string
562+
const unsupportedLanguage = language ? language.charAt(0).toUpperCase() + language.slice(1) : ''
563+
if (!workspaceFolder) {
564+
// File is outside of workspace
565+
unsupportedMessage = `<span style="color: #EE9D28;">&#9888;<b>I can't generate tests for ${fileName}</b> because the file is outside of workspace scope.<br></span> I can still provide examples, instructions and code suggestions.`
566+
} else if (unsupportedLanguage) {
567+
unsupportedMessage = `<span style="color: #EE9D28;">&#9888;<b>I'm sorry, but /test only supports Python and Java</b><br></span> While ${unsupportedLanguage} is not supported, I will generate a suggestion below.`
568+
} else {
569+
unsupportedMessage = `<span style="color: #EE9D28;">&#9888;<b>I'm sorry, but /test only supports Python and Java</b><br></span> I will still generate a suggestion below.`
570+
}
571+
this.messenger.sendMessage(unsupportedMessage, tabID, 'answer')
572+
session.isSupportedLanguage = false
573+
await this.onCodeGeneration(
574+
session,
575+
userPrompt,
576+
tabID,
577+
fileName,
578+
filePath,
579+
workspaceFolder !== undefined
581580
)
581+
} else {
582+
this.messenger.sendCapabilityCard({ tabID })
583+
this.messenger.sendMessage(testGenSummaryMessage(fileName), message.tabID, 'answer-part')
584+
585+
// Grab the selection from the fileEditorToTest and get the vscode Range
586+
const selection = fileEditorToTest.selection
587+
let selectionRange = undefined
588+
if (
589+
selection.start.line !== selection.end.line ||
590+
selection.start.character !== selection.end.character
591+
) {
592+
selectionRange = new vscode.Range(
593+
selection.start.line,
594+
selection.start.character,
595+
selection.end.line,
596+
selection.end.character
597+
)
598+
}
599+
session.isCodeBlockSelected = selectionRange !== undefined
600+
session.isSupportedLanguage = true
601+
602+
/**
603+
* Zip the project
604+
* Create pre-signed URL and upload artifact to S3
605+
* send API request to startTestGeneration API
606+
* Poll from getTestGeneration API
607+
* Get Diff from exportResultArchive API
608+
*/
609+
ChatSessionManager.Instance.setIsInProgress(true)
610+
await startTestGenerationProcess(filePath, message.prompt, tabID, true, selectionRange)
582611
}
583-
session.isCodeBlockSelected = selectionRange !== undefined
584-
session.isSupportedLanguage = true
585-
586-
/**
587-
* Zip the project
588-
* Create pre-signed URL and upload artifact to S3
589-
* send API request to startTestGeneration API
590-
* Poll from getTestGeneration API
591-
* Get Diff from exportResultArchive API
592-
*/
593-
ChatSessionManager.Instance.setIsInProgress(true)
594-
await startTestGenerationProcess(filePath, message.prompt, tabID, true, selectionRange)
595612
}
596613
} catch (err: any) {
597614
// TODO: refactor error handling to be more robust
@@ -639,6 +656,32 @@ export class TestController {
639656
}
640657
}
641658

659+
/**
660+
* Process a user message using Q CLI streaming functionality
661+
*/
662+
private async processQCliStreamingMessage(data: { prompt: string; tabID: string }): Promise<void> {
663+
const session = this.sessionStorage.getSession()
664+
const logger = getLogger()
665+
666+
try {
667+
logger.debug('Processing Q CLI streaming message: %s', data.prompt)
668+
669+
// // Check authentication
670+
// const authState = await AuthUtil.instance.getChatAuthState()
671+
// if (authState.amazonQ !== 'connected') {
672+
// void this.messenger.sendAuthNeededExceptionMessage(authState, data.tabID)
673+
// session.isAuthenticating = true
674+
// return
675+
// }
676+
677+
// Process the user prompt through the streaming service
678+
await QCliStreamingService.getInstance().processUserPrompt(data.prompt, data.tabID, session)
679+
} catch (error) {
680+
logger.error('Error processing Q CLI streaming message: %O', error)
681+
this.messenger.sendErrorMessage('Failed to process your request. Please try again.', data.tabID)
682+
}
683+
}
684+
642685
private async updateTargetFileInfo(message: {
643686
tabID: string
644687
targetFileInfo?: TargetFileInfo
@@ -1410,7 +1453,10 @@ export class TestController {
14101453
if (session.tabID) {
14111454
getLogger().debug('Setting input state with tabID: %s', session.tabID)
14121455
this.messenger.sendChatInputEnabled(session.tabID, true)
1413-
this.messenger.sendUpdatePlaceholder(session.tabID, 'Enter "/" for quick actions')
1456+
this.messenger.sendUpdatePlaceholder(
1457+
session.tabID,
1458+
'Ask Amazon Q a question or use /test to generate unit tests...'
1459+
)
14141460
}
14151461
getLogger().debug(
14161462
'Deleting output.log and temp result directory. testGenerationLogsDir: %s',
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// private async handleUpdatePromptProgress(data: any) {
7+
// this.messenger.sendUpdatePromptProgress(data.tabID, data.status === 'cancel' ? cancellingProgressField : null)
8+
// }
9+
10+
// /**
11+
// * Process a user message using Q CLI streaming functionality
12+
// */
13+
// private async processQCliStreamingMessage(data: { prompt: string; tabID: string }): Promise<void> {
14+
// const session = this.sessionStorage.getSession()
15+
// const logger = getLogger()
16+
17+
// try {
18+
// logger.debug('Processing Q CLI streaming message: %s', data.prompt)
19+
20+
// // Check authentication
21+
// const authState = await AuthUtil.instance.getChatAuthState()
22+
// if (authState.amazonQ !== 'connected') {
23+
// void this.messenger.sendAuthNeededExceptionMessage(authState, data.tabID)
24+
// session.isAuthenticating = true
25+
// return
26+
// }
27+
28+
// // Process the user prompt through the streaming service
29+
// await QCliStreamingService.getInstance().processUserPrompt(data.prompt, data.tabID, session)
30+
// } catch (error) {
31+
// logger.error('Error processing Q CLI streaming message: %O', error)
32+
// this.messenger.sendErrorMessage('Failed to process your request. Please try again.', data.tabID)
33+
// }
34+
// }
35+
/* [object Object]*/

0 commit comments

Comments
 (0)