Skip to content

Commit b9de669

Browse files
committed
featureDev: Handle invalidStateEvent during planning
1 parent 3acc297 commit b9de669

File tree

6 files changed

+47
-20
lines changed

6 files changed

+47
-20
lines changed

packages/core/src/amazonqFeatureDev/client/featureDev.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { CodeReference } from '../../amazonq/webview/ui/connector'
1818
import { ApiError, ContentLengthError, UnknownApiError } from '../errors'
1919
import { ToolkitError, isAwsError, isCodeWhispererStreamingServiceException } from '../../shared/errors'
2020
import { getCodewhispererConfig } from '../../codewhisperer/client/codewhisperer'
21+
import { LLMResponseType } from '../types'
2122

2223
// Create a client for featureDev proxy client based off of aws sdk v2
2324
export async function createFeatureDevProxyClient(): Promise<FeatureDevProxyClient> {
@@ -136,7 +137,13 @@ export class FeatureDevClient {
136137
throw new UnknownApiError(e instanceof Error ? e.message : 'Unknown error', 'CreateUploadUrl')
137138
}
138139
}
139-
public async generatePlan(conversationId: string, uploadId: string, userMessage: string) {
140+
public async generatePlan(
141+
conversationId: string,
142+
uploadId: string,
143+
userMessage: string
144+
): Promise<
145+
{ responseType: 'EMPTY'; approach?: string } | { responseType: 'VALID' | 'INVALID_STATE'; approach: string }
146+
> {
140147
try {
141148
const streamingClient = await this.getStreamingClient()
142149
const params = {
@@ -155,8 +162,9 @@ export class FeatureDevClient {
155162
getLogger().debug(`${featureName}: Generated plan: %O`, {
156163
requestId: response.$metadata.requestId,
157164
})
165+
let responseType: LLMResponseType = 'EMPTY'
158166
if (!response.planningResponseStream) {
159-
return undefined
167+
return { responseType }
160168
}
161169

162170
const assistantResponse: string[] = []
@@ -167,12 +175,14 @@ export class FeatureDevClient {
167175
getLogger().debug('Received Invalid State Event: %O', responseItem.invalidStateEvent)
168176
assistantResponse.splice(0)
169177
assistantResponse.push(responseItem.invalidStateEvent.message ?? '')
178+
responseType = 'INVALID_STATE'
170179
break
171180
} else if (responseItem.assistantResponseEvent !== undefined) {
181+
responseType = 'VALID'
172182
assistantResponse.push(responseItem.assistantResponseEvent.content ?? '')
173183
}
174184
}
175-
return assistantResponse.join('')
185+
return { responseType, approach: assistantResponse.join('') }
176186
} catch (e) {
177187
if (isCodeWhispererStreamingServiceException(e)) {
178188
getLogger().error(

packages/core/src/amazonqFeatureDev/controllers/chat/controller.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -262,18 +262,20 @@ export class FeatureDevController {
262262
canBeVoted: true,
263263
})
264264

265-
this.messenger.sendAnswer({
266-
type: 'answer',
267-
tabID,
268-
message: `Would you like to generate a suggestion for this? You’ll review a file diff before inserting into your project.`,
269-
})
265+
if (interactions.responseType === 'VALID') {
266+
this.messenger.sendAnswer({
267+
type: 'answer',
268+
tabID,
269+
message: `Would you like to generate a suggestion for this? You’ll review a file diff before inserting into your project.`,
270+
})
270271

271-
// Follow up with action items and complete the request stream
272-
this.messenger.sendAnswer({
273-
type: 'system-prompt', // show the followups on the right side
274-
followUps: this.getFollowUpOptions(session.state.phase),
275-
tabID: tabID,
276-
})
272+
// Follow up with action items and complete the request stream
273+
this.messenger.sendAnswer({
274+
type: 'system-prompt', // show the followups on the right side
275+
followUps: this.getFollowUpOptions(session.state.phase),
276+
tabID: tabID,
277+
})
278+
}
277279

278280
// Unlock the prompt again so that users can iterate
279281
this.messenger.sendAsyncEventProgress(tabID, false, undefined)

packages/core/src/amazonqFeatureDev/session/sessionState.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ export class RefinementState implements SessionState {
115115
throw new UserMessageNotFoundError()
116116
}
117117

118-
const approach = await this.config.proxyClient.generatePlan(
118+
const { responseType, approach } = await this.config.proxyClient.generatePlan(
119119
this.config.conversationId,
120120
this.config.uploadId,
121121
action.msg
@@ -126,7 +126,7 @@ export class RefinementState implements SessionState {
126126
'There has been a problem generating an approach. Please open a conversation in a new tab'
127127
)
128128

129-
action.telemetry.recordUserApproachTelemetry(span, this.conversationId)
129+
action.telemetry.recordUserApproachTelemetry(span, this.conversationId, responseType)
130130
return {
131131
nextState: new RefinementState(
132132
{
@@ -139,6 +139,7 @@ export class RefinementState implements SessionState {
139139
),
140140
interaction: {
141141
content: `${this.approach}\n`,
142+
responseType,
142143
},
143144
}
144145
} catch (e) {

packages/core/src/amazonqFeatureDev/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { CodeReference } from '../amazonq/webview/ui/connector'
1515
export type Interaction = {
1616
// content to be sent back to the chat UI
1717
content?: string
18+
responseType?: LLMResponseType
1819
}
1920

2021
export interface SessionStateInteraction {
@@ -95,3 +96,5 @@ export function createUri(filePath: string, tabID?: string) {
9596
...(tabID ? { query: `tabID=${tabID}` } : {}),
9697
})
9798
}
99+
100+
export type LLMResponseType = 'EMPTY' | 'INVALID_STATE' | 'VALID'

packages/core/src/amazonqFeatureDev/util/telemetryHelper.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { getLogger } from '../../shared/logger/logger'
77
import { AmazonqApproachInvoke, AmazonqCodeGenerationInvoke, Metric } from '../../shared/telemetry/telemetry'
8+
import { LLMResponseType } from '../types'
89

910
const performance = globalThis.performance ?? require('perf_hooks').performance
1011

@@ -31,11 +32,16 @@ export class TelemetryHelper {
3132
this.sessionStartTime = performance.now()
3233
}
3334

34-
public recordUserApproachTelemetry(span: Metric<AmazonqApproachInvoke>, amazonqConversationId: string) {
35+
public recordUserApproachTelemetry(
36+
span: Metric<AmazonqApproachInvoke>,
37+
amazonqConversationId: string,
38+
responseType: LLMResponseType
39+
) {
3540
const event = {
3641
amazonqConversationId,
3742
amazonqGenerateApproachIteration: this.generateApproachIteration,
3843
amazonqGenerateApproachLatency: performance.now() - this.generateApproachLastInvocationTime,
44+
amazonqGenerateApproachResponseType: responseType,
3945
}
4046
getLogger().debug(`recordUserApproachTelemetry: %O`, event)
4147
span.record(event)

packages/core/src/test/amazonqFeatureDev/session/sessionState.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,15 @@ describe('sessionState', () => {
118118
it('transitions to RefinementState and returns an approach', async () => {
119119
sinon.stub(performance, 'now').returns(0)
120120

121-
mockGeneratePlan = sinon.stub().resolves(testApproach)
121+
mockGeneratePlan = sinon.stub().resolves({ responseType: 'TEST_RESPONSE_TYPE', approach: testApproach })
122122
const state = new RefinementState(testConfig, testApproach, tabId, 0)
123123
const result = await state.interact(testAction)
124124

125125
assert.deepStrictEqual(result, {
126126
nextState: new RefinementState(testConfig, testApproach, tabId, 1),
127127
interaction: {
128128
content: `${testApproach}\n`,
129+
responseType: 'TEST_RESPONSE_TYPE',
129130
},
130131
})
131132

@@ -138,7 +139,7 @@ describe('sessionState', () => {
138139
})
139140

140141
it('transitions to RefinementState but does not return an approach', async () => {
141-
mockGeneratePlan = sinon.stub().resolves(undefined)
142+
mockGeneratePlan = sinon.stub().resolves({ responseType: 'TEST_RESPONSE_TYPE', approach: undefined })
142143
const state = new RefinementState(testConfig, testApproach, tabId, 0)
143144
const result = await state.interact(testAction)
144145
const invokeFailureApproach =
@@ -148,14 +149,17 @@ describe('sessionState', () => {
148149
nextState: new RefinementState(testConfig, invokeFailureApproach, tabId, 1),
149150
interaction: {
150151
content: `${invokeFailureApproach}\n`,
152+
responseType: 'TEST_RESPONSE_TYPE',
151153
},
152154
})
153155
})
154156

155157
it('invalid html gets sanitized', async () => {
156158
const invalidHTMLApproach =
157159
'<head><script src="https://foo"></script></head><body><h1>hello world</h1></body>'
158-
mockGeneratePlan = sinon.stub().resolves(invalidHTMLApproach)
160+
mockGeneratePlan = sinon
161+
.stub()
162+
.resolves({ responseType: 'TEST_RESPONSE_TYPE', approach: invalidHTMLApproach })
159163
const state = new RefinementState(testConfig, invalidHTMLApproach, tabId, 0)
160164
const result = await state.interact(testAction)
161165

@@ -165,6 +169,7 @@ describe('sessionState', () => {
165169
nextState: new RefinementState(testConfig, expectedApproach, tabId, 1),
166170
interaction: {
167171
content: `${expectedApproach}\n`,
172+
responseType: 'TEST_RESPONSE_TYPE',
168173
},
169174
})
170175
})

0 commit comments

Comments
 (0)