Skip to content

Commit

Permalink
Merge pull request #5307 from aws/feature/dev-execution
Browse files Browse the repository at this point in the history
Amazon Q: Allow customers to configure the ability to run code and tests with /dev
  • Loading branch information
rli authored Jan 28, 2025
2 parents 445dd9e + 5adb242 commit 0cd4bca
Show file tree
Hide file tree
Showing 21 changed files with 407 additions and 110 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 @@ -39,7 +39,7 @@ class PrepareDocGenerationState(
var zipFileLength: Long? = null
val nextState: SessionState
try {
val repoZipResult = config.repoContext.getProjectZip()
val repoZipResult = config.repoContext.getProjectZip(false)
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 @@ -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 commands are install, build and test (are all optional). so you may have to bundle some commands together using '&&'. 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: install exec: component: dev commandLine: \"npm install\" - id: build exec: component: dev commandLine: \"npm run build\" - id: test exec: component: dev commandLine: \"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 @@ -32,11 +32,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 @@ -77,6 +79,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content
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 @@ -135,6 +138,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 @@ -440,20 +453,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 @@ -471,9 +502,7 @@ class FeatureDevController(
}
}

private suspend fun newTask(tabId: String, isException: Boolean? = false) {
this.disablePreviousFileList(tabId)

private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) {
val session = getSessionInfo(tabId)
val sessionLatency = System.currentTimeMillis() - session.sessionStartTime

Expand All @@ -485,15 +514,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")
)
}
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 @@ -670,6 +714,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 @@ -682,8 +727,18 @@ class FeatureDevController(
return
}

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

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

session.preloader(messenger)
broadcastQEvent(QFeatureEvent.INVOCATION)

when (session.sessionState.phase) {
SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId)
else -> null
Expand All @@ -696,6 +751,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"),
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 @@ -250,6 +250,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 @@ -48,7 +49,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 Expand Up @@ -96,7 +98,7 @@ class PrepareCodeGenerationState(
credentialStartUrl = getStartUrl(config.featureDevService.project)
)
}
// It is essential to interact with the next state outside of try-catch block for the telemetry to capture events for the states separately
// It is essential to interact with the next state outside try-catch block for the telemetry to capture events for the states separately
return nextState.interact(action)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,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 @@ -79,10 +79,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 @@ -281,8 +278,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
Loading

0 comments on commit 0cd4bca

Please sign in to comment.