Skip to content

Commit

Permalink
feat(feature dev): Add setting to allow Q to run code and test commands
Browse files Browse the repository at this point in the history
  • Loading branch information
willyyhuang committed Nov 13, 2024
1 parent dfb6a83 commit 5fcc010
Show file tree
Hide file tree
Showing 16 changed files with 198 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "Add setting to allow Q /dev to run code and test commands"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const val FEATURE_EVALUATION_PRODUCT_NAME = "FeatureDev"

const val FEATURE_NAME = "Amazon Q Developer Agent for software development"

@Suppress("MaxLineLength")
const val GENERATE_DEV_FILE_PROMPT = "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported command is test, so you should bundle all install, build and test commands in “test”. also you can use “public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: test exec: component: dev commandLine: “npm install && npm run build && npm run test”"

// Max number of times a user can attempt to retry a code generation request if it fails
const val CODE_GENERATION_RETRY_LIMIT = 3

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
Expand Down Expand Up @@ -69,6 +71,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFol
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
import software.aws.toolkits.jetbrains.utils.notifyError
import software.aws.toolkits.resources.message
Expand Down Expand Up @@ -121,6 +124,16 @@ class FeatureDevController(
FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId)
FollowUpTypes.NEW_TASK -> newTask(message.tabId)
FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId)
FollowUpTypes.ACCEPT_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, true)
FollowUpTypes.DENY_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, false)
FollowUpTypes.GENERATE_DEV_FILE -> {
messenger.sendAnswer(
tabId = message.tabId,
messageType = FeatureDevMessageType.SystemPrompt,
message = message("amazonqFeatureDev.follow_up.generate_dev_file")
)
newTask(tabId = message.tabId, prefilledPrompt = GENERATE_DEV_FILE_PROMPT)
}
}
}

Expand Down Expand Up @@ -345,20 +358,38 @@ class FeatureDevController(
canBeVoted = true
)

messenger.sendSystemPrompt(
tabId = tabId,
followUp = listOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.new_task"),
type = FollowUpTypes.NEW_TASK,
status = FollowUpStatusType.Info
),
val followUps = mutableListOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.new_task"),
type = FollowUpTypes.NEW_TASK,
status = FollowUpStatusType.Info
),
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.close_session"),
type = FollowUpTypes.CLOSE_SESSION,
status = FollowUpStatusType.Info
),
)

if (!session.context.checkForDevFile()) {
followUps.add(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.close_session"),
type = FollowUpTypes.CLOSE_SESSION,
pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"),
type = FollowUpTypes.GENERATE_DEV_FILE,
status = FollowUpStatusType.Info
)
)

messenger.sendAnswer(
tabId = tabId,
message = message("amazonqFeatureDev.chat_message.generate_dev_file"),
messageType = FeatureDevMessageType.Answer
)
}

messenger.sendSystemPrompt(
tabId = tabId,
followUp = followUps
)

messenger.sendUpdatePlaceholder(
Expand All @@ -376,7 +407,7 @@ class FeatureDevController(
}
}

private suspend fun newTask(tabId: String, isException: Boolean? = false) {
private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
val session = getSessionInfo(tabId)
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime
AmazonqTelemetry.endChat(
Expand All @@ -387,15 +418,30 @@ class FeatureDevController(
chatSessionStorage.deleteSession(tabId)

newTabOpened(tabId)
if (isException != null && !isException) {
messenger.sendAnswer(
tabId = tabId,
messageType = FeatureDevMessageType.Answer,
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")
)

if (prefilledPrompt != null && isException != null && !isException) {
handleChat(tabId = tabId, message = prefilledPrompt)
} else {
if (isException != null && !isException) {
messenger.sendAnswer(
tabId = tabId,
messageType = FeatureDevMessageType.Answer,
message = message("amazonqFeatureDev.chat_message.ask_for_new_task")

Check warning on line 429 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

View workflow job for this annotation

GitHub Actions / qodana

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
)
}
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
}
messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan"))
messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true)
}

private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) {
CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value)
messenger.sendAnswer(
tabId = tabId,
message = message("amazonqFeatureDev.chat_message.setting_updated"),
messageType = FeatureDevMessageType.Answer,
)
this.retryRequests(tabId)
}

private suspend fun closeSession(tabId: String) {
Expand Down Expand Up @@ -554,6 +600,7 @@ class FeatureDevController(
try {
logger.debug { "$FEATURE_NAME: Processing message: $message" }
session = getSessionInfo(tabId)
session.latestMessage = message

val credentialState = authController.getAuthNeededStates(context.project).amazonQ
if (credentialState != null) {
Expand All @@ -566,7 +613,16 @@ class FeatureDevController(
return
}

session.preloader(message, messenger)
val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration()
val hasDevFile = session.context.checkForDevFile()
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot())

if (hasDevFile && !isPromptedForAutoBuildFeature) {
promptAllowQCommandsConsent(messenger, tabId)
return
}

session.preloader(messenger)

when (session.sessionState.phase) {
SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId)
Expand All @@ -580,6 +636,30 @@ class FeatureDevController(
}
}

private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher, tabID: String) {
messenger.sendAnswer(
tabId = tabID,
message = message("amazonqFeatureDev.chat_message.devFileInRepository"),
messageType = FeatureDevMessageType.Answer
)
messenger.sendAnswer(
tabId = tabID,
messageType = FeatureDevMessageType.SystemPrompt,
followUp = listOf(
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.accept_for_project"),

Check warning on line 650 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

View workflow job for this annotation

GitHub Actions / qodana

Usage of redundant or deprecated syntax or deprecated symbols

'message(String, vararg Any): String' is deprecated. Use extension-specific localization bundle instead
type = FollowUpTypes.ACCEPT_AUTO_BUILD,
status = FollowUpStatusType.Success
),
FollowUp(
pillText = message("amazonqFeatureDev.follow_up.decline_for_project"),
type = FollowUpTypes.DENY_AUTO_BUILD,
status = FollowUpStatusType.Error
)
)
)
}

private suspend fun retryRequests(tabId: String) {
var session: Session? = null
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,9 @@ enum class FollowUpTypes(
PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"),
NEW_TASK("NewTask"),
CLOSE_SESSION("CloseSession"),
ACCEPT_AUTO_BUILD("AcceptAutoBuild"),
DENY_AUTO_BUILD("DenyAutoBuild"),
GENERATE_DEV_FILE("GenerateDevFile"),
}

// Util classes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.Cancellat
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.deleteUploadArtifact
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
import software.aws.toolkits.resources.message
import software.aws.toolkits.telemetry.AmazonqTelemetry
import software.aws.toolkits.telemetry.AmazonqUploadIntent
Expand Down Expand Up @@ -47,7 +48,8 @@ class PrepareCodeGenerationState(
messenger.sendAnswerPart(tabId = this.tabID, message = message("amazonqFeatureDev.chat_message.uploading_code"))
messenger.sendUpdatePlaceholder(tabId = this.tabID, newPlaceholder = message("amazonqFeatureDev.chat_message.uploading_code"))

val repoZipResult = config.repoContext.getProjectZip()
val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.getWorkspaceRoot())
val repoZipResult = config.repoContext.getProjectZip(isAutoBuildFeatureEnabled = isAutoBuildFeatureEnabled)
val zipFileChecksum = repoZipResult.checksum
zipFileLength = repoZipResult.contentLength
val fileToUpload = repoZipResult.payload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ class Session(val tabID: String, val project: Project) {
/**
* Preload any events that have to run before a chat message can be sent
*/
suspend fun preloader(msg: String, messenger: MessagePublisher) {
suspend fun preloader(messenger: MessagePublisher) {
if (!preloaderFinished) {
setupConversation(msg, messenger)
setupConversation(messenger)
preloaderFinished = true
messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true)
featureDevService.sendFeatureDevEvent(this.conversationId)
Expand All @@ -64,10 +64,7 @@ class Session(val tabID: String, val project: Project) {
/**
* Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it.
*/
private fun setupConversation(msg: String, messenger: MessagePublisher) {
// Store the initial message when setting up the conversation so that if it fails we can retry with this message
_latestMessage = msg

private fun setupConversation(messenger: MessagePublisher) {
_conversationId = featureDevService.createConversation()
logger<Session>().info(conversationIDLog(this.conversationId))

Expand Down Expand Up @@ -159,8 +156,11 @@ class Session(val tabID: String, val project: Project) {
}
}

val latestMessage: String
var latestMessage: String
get() = this._latestMessage
set(value) {
this._latestMessage = value
}

val retries: Int
get() = codegenRetries
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase() {
@Test
fun testWithInvalidFile() {
val txtFile = mock<VirtualFile>()
whenever(txtFile.extension).thenReturn("txt")
whenever(txtFile.extension).thenReturn("mp4")
assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs

runTest {
spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)
}

Expand Down Expand Up @@ -188,7 +188,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
mockkObject(AmazonqTelemetry)
every { AmazonqTelemetry.isProvideFeedbackForCodeGen(amazonqConversationId = any(), enabled = any()) } just runs

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

coVerifyOrder {
Expand Down Expand Up @@ -240,7 +240,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {

doNothing().`when`(spySession).insertChanges(any(), any(), any())

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

mockitoVerify(
Expand All @@ -265,6 +265,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
listOf(
FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info),
FollowUp(FollowUpTypes.CLOSE_SESSION, message("amazonqFeatureDev.follow_up.close_session"), status = FollowUpStatusType.Info),
FollowUp(FollowUpTypes.GENERATE_DEV_FILE, message("amazonqFeatureDev.follow_up.generate_dev_file"), status = FollowUpStatusType.Info)
),
)
messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements"))
Expand Down Expand Up @@ -447,7 +448,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns null

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

coVerifyOrder {
Expand Down Expand Up @@ -478,7 +479,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

coVerifyOrder {
Expand Down Expand Up @@ -515,7 +516,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
every { selectFolder(any(), any()) } returns folder

spySession.preloader(userMessage, messenger)
spySession.preloader(messenger)
controller.processFollowupClickedMessage(message)

coVerify {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,14 @@ class PrepareCodeGenerationStateTest : FeatureDevTestBase() {
val repoZipResult = ZipCreationResult(mockFile, testChecksumSha, testContentLength)
val action = SessionStateAction("test-task", userMessage)

whenever(repoContext.getProjectZip()).thenReturn(repoZipResult)
whenever(repoContext.getProjectZip(false)).thenReturn(repoZipResult)
every { featureDevService.createUploadUrl(any(), any(), any(), any()) } returns exampleCreateUploadUrlResponse

runTest {
val actual = prepareCodeGenerationState.interact(action)
assertThat(actual.nextState).isInstanceOf(PrepareCodeGenerationState::class.java)
}
assertThat(prepareCodeGenerationState.phase).isEqualTo(SessionStatePhase.CODEGEN)
verify(repoContext, times(1)).getProjectZip()
verify(repoContext, times(1)).getProjectZip(false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class SessionTest : FeatureDevTestBase() {
fun `test preloader`() = runTest {
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)

session.preloader(userMessage, messenger)
session.preloader(messenger)
assertThat(session.conversationId).isEqualTo(testConversationId)
assertThat(session.sessionState).isInstanceOf(PrepareCodeGenerationState::class.java)
verify(featureDevClient, times(1)).createTaskAssistConversation()
Expand Down
Loading

0 comments on commit 5fcc010

Please sign in to comment.