Skip to content

Commit 0e215fc

Browse files
feat: memory bank support (aws#2314)
* feat: support memory bank (aws#2299) * feat: poc for adding memory bank as rules * feat: changed to use simlar prompt from kiro * feat: implement iteration based guidelines generation from science doc * fix: fixing issues from poc code * fix: refine UX messages * fix: improving memory bank pre-processing logic (aws#2301) * fix: limit file size for during memory bank analysis (aws#2304) * fix: ehance memory bank prompt for more determinastic file name and path (aws#2305) * fix: fix windows path issue for memory bank (aws#2307) * fix: small path fix for button text, remove unnecessary logs (aws#2310) --------- Co-authored-by: aws-toolkit-automation <[email protected]>
1 parent fe128b6 commit 0e215fc

File tree

7 files changed

+1412
-10
lines changed

7 files changed

+1412
-10
lines changed

chat-client/src/client/features/rules.test.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ describe('rules', () => {
2222

2323
messager = {
2424
onRuleClick: sinon.stub(),
25+
onChatPrompt: sinon.stub(),
2526
} as unknown as Messager
2627

2728
rulesList = new RulesList(mynahUi, messager)
@@ -142,6 +143,22 @@ describe('rules', () => {
142143
assert.equal(formArgs[2][1].id, ContextRule.SubmitButtonId)
143144
})
144145

146+
it('calls messager when create memory bank is clicked', () => {
147+
const createMemoryBankItem: DetailedListItem = {
148+
id: ContextRule.CreateMemoryBankId,
149+
description: 'Generate Memory Bank',
150+
}
151+
152+
onItemClick(createMemoryBankItem)
153+
154+
// Should send a chat prompt
155+
sinon.assert.calledOnce(messager.onChatPrompt as sinon.SinonStub)
156+
157+
const chatPromptArgs = (messager.onChatPrompt as sinon.SinonStub).getCall(0).args[0]
158+
assert.equal(chatPromptArgs.prompt.prompt, 'Generate a Memory Bank for this project')
159+
assert.equal(chatPromptArgs.prompt.escapedPrompt, 'Generate a Memory Bank for this project')
160+
})
161+
145162
it('calls messager when regular rule is clicked', () => {
146163
const ruleItem: DetailedListItem = {
147164
id: 'test-rule-id',
@@ -267,21 +284,27 @@ describe('rules', () => {
267284

268285
const result = convertRulesListToDetailedListGroup(rulesFolder)
269286

270-
assert.equal(result.length, 3) // 2 folders + create rule group
287+
assert.equal(result.length, 3) // 2 folders + actions group
271288
assert.equal(result[0].groupName, 'test-folder')
272289
assert.equal(result[0].children?.length, 2)
273290
assert.equal(result[0].children?.[0].id, 'rule-1')
274291
assert.equal(result[0].children?.[0].description, 'Test Rule 1')
275292
assert.equal(result[1].groupName, 'inactive-folder')
276293
assert.equal(result[1].children?.length, 0)
277-
assert.equal(result[2].children?.[0].id, ContextRule.CreateRuleId)
294+
assert.equal(result[2].groupName, 'Actions')
295+
assert.equal(result[2].children?.length, 2) // Memory Bank + Create Rule
296+
assert.equal(result[2].children?.[0].id, ContextRule.CreateMemoryBankId)
297+
assert.equal(result[2].children?.[1].id, ContextRule.CreateRuleId)
278298
})
279299

280300
it('handles empty rules array', () => {
281301
const result = convertRulesListToDetailedListGroup([])
282302

283-
assert.equal(result.length, 1) // Only create rule group
284-
assert.equal(result[0].children?.[0].id, ContextRule.CreateRuleId)
303+
assert.equal(result.length, 1) // Only actions group
304+
assert.equal(result[0].groupName, 'Actions')
305+
assert.equal(result[0].children?.length, 2) // Memory Bank + Create Rule
306+
assert.equal(result[0].children?.[0].id, ContextRule.CreateMemoryBankId)
307+
assert.equal(result[0].children?.[1].id, ContextRule.CreateRuleId)
285308
})
286309
})
287310
})

chat-client/src/client/features/rules.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MynahDetailedList } from './history'
66

77
export const ContextRule = {
88
CreateRuleId: 'create-rule',
9+
CreateMemoryBankId: 'create-memory-bank',
910
CancelButtonId: 'cancel-create-rule',
1011
SubmitButtonId: 'submit-create-rule',
1112
RuleNameFieldId: 'rule-name',
@@ -68,12 +69,29 @@ export class RulesList {
6869
],
6970
`Create a rule`
7071
)
72+
} else if (item.id === ContextRule.CreateMemoryBankId) {
73+
this.rulesList?.close()
74+
this.handleMemoryBankCreation()
7175
} else {
7276
this.messager.onRuleClick({ tabId: this.tabId, type: 'rule', id: item.id })
7377
}
7478
}
7579
}
7680

81+
private handleMemoryBankCreation = () => {
82+
// Close the rules list first
83+
this.rulesList?.close()
84+
85+
// Use the current tab, the tabId should be the same as the one used for the rules list
86+
this.messager.onChatPrompt({
87+
prompt: {
88+
prompt: 'Generate a Memory Bank for this project',
89+
escapedPrompt: 'Generate a Memory Bank for this project',
90+
},
91+
tabId: this.tabId,
92+
})
93+
}
94+
7795
showLoading(tabId: string) {
7896
this.tabId = tabId
7997
const rulesList = this.mynahUi.openTopBarButtonOverlay({
@@ -156,6 +174,24 @@ const createRuleListItem: DetailedListItem = {
156174
id: ContextRule.CreateRuleId,
157175
}
158176

177+
function createMemoryBankListItem(rules: RulesFolder[]): DetailedListItem {
178+
// Handles button text changes between "Generation" and "Regenerate"
179+
const memoryBankFiles = ['product', 'structure', 'tech', 'guidelines']
180+
181+
const memoryBankFolder = rules.find(folder => folder.folderName === 'memory-bank')
182+
183+
const hasMemoryBankFiles =
184+
memoryBankFolder && memoryBankFolder.rules.some(rule => memoryBankFiles.includes(rule.name))
185+
186+
const buttonText = hasMemoryBankFiles ? 'Regenerate Memory Bank' : 'Generate Memory Bank'
187+
188+
return {
189+
description: buttonText,
190+
icon: MynahIcons.FOLDER,
191+
id: ContextRule.CreateMemoryBankId,
192+
}
193+
}
194+
159195
export function convertRulesListToDetailedListGroup(rules: RulesFolder[]): DetailedListItemGroup[] {
160196
return rules
161197
.map(
@@ -179,7 +215,10 @@ export function convertRulesListToDetailedListGroup(rules: RulesFolder[]): Detai
179215
})),
180216
}) as DetailedListItemGroup
181217
)
182-
.concat({ children: [createRuleListItem] })
218+
.concat({
219+
groupName: 'Actions',
220+
children: [createMemoryBankListItem(rules), createRuleListItem],
221+
})
183222
}
184223

185224
function convertRuleStatusToIcon(status: boolean | 'indeterminate'): MynahIcons | undefined {

server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ import { IDE } from '../../shared/constants'
231231
import { IdleWorkspaceManager } from '../workspaceContext/IdleWorkspaceManager'
232232
import escapeHTML = require('escape-html')
233233
import { SemanticSearch } from './tools/workspaceContext/semanticSearch'
234+
import { MemoryBankController } from './context/memorybank/memoryBankController'
234235

235236
type ChatHandlers = Omit<
236237
LspHandlers<Chat>,
@@ -267,6 +268,7 @@ export class AgenticChatController implements ChatHandlers {
267268
#chatHistoryDb: ChatDatabase
268269
#additionalContextProvider: AdditionalContextProvider
269270
#contextCommandsProvider: ContextCommandsProvider
271+
#memoryBankController: MemoryBankController
270272
#stoppedToolUses = new Set<string>()
271273
#userWrittenCodeTracker: UserWrittenCodeTracker | undefined
272274
#toolUseStartTimes: Record<string, number> = {}
@@ -370,6 +372,7 @@ export class AgenticChatController implements ChatHandlers {
370372
this.#mcpEventHandler = new McpEventHandler(features, telemetryService)
371373
this.#origin = getOriginFromClientInfo(getClientName(this.#features.lsp.getClientInitializeParams()))
372374
this.#activeUserTracker = ActiveUserTracker.getInstance(this.#features)
375+
this.#memoryBankController = MemoryBankController.getInstance(features)
373376
}
374377

375378
async onExecuteCommand(params: ExecuteCommandParams, _token: CancellationToken): Promise<any> {
@@ -833,6 +836,93 @@ export class AgenticChatController implements ChatHandlers {
833836

834837
IdleWorkspaceManager.recordActivityTimestamp()
835838

839+
// Memory Bank Creation Flow - Delegate to MemoryBankController
840+
if (this.#memoryBankController.isMemoryBankCreationRequest(params.prompt.prompt)) {
841+
this.#features.logging.info(`Memory Bank creation request detected for tabId: ${params.tabId}`)
842+
843+
// Store original prompt to prevent data loss on failure
844+
const originalPrompt = params.prompt.prompt
845+
846+
try {
847+
const workspaceFolders = workspaceUtils.getWorkspaceFolderPaths(this.#features.workspace)
848+
const workspaceUri = workspaceFolders.length > 0 ? workspaceFolders[0] : ''
849+
850+
if (!workspaceUri) {
851+
throw new Error('No workspace folder found for Memory Bank creation')
852+
}
853+
854+
// Check if memory bank already exists to provide appropriate user feedback
855+
const memoryBankExists = await this.#memoryBankController.memoryBankExists(workspaceUri)
856+
const actionType = memoryBankExists ? 'Regenerating' : 'Generating'
857+
this.#features.logging.info(`${actionType} Memory Bank for workspace: ${workspaceUri}`)
858+
859+
const resultStream = this.#getChatResultStream(params.partialResultToken)
860+
await resultStream.writeResultBlock({
861+
body: `Preparing to analyze your project...`,
862+
type: 'answer',
863+
messageId: crypto.randomUUID(),
864+
})
865+
866+
const comprehensivePrompt = await this.#memoryBankController.prepareComprehensiveMemoryBankPrompt(
867+
workspaceUri,
868+
async (prompt: string) => {
869+
// Direct LLM call for ranking - no agentic loop
870+
try {
871+
if (!this.#serviceManager) {
872+
throw new Error('amazonQServiceManager is not initialized')
873+
}
874+
875+
const client = this.#serviceManager.getStreamingClient()
876+
const requestInput: SendMessageCommandInput = {
877+
conversationState: {
878+
chatTriggerType: ChatTriggerType.MANUAL,
879+
currentMessage: {
880+
userInputMessage: {
881+
content: prompt,
882+
},
883+
},
884+
},
885+
}
886+
887+
const response = await client.sendMessage(requestInput)
888+
889+
let responseContent = ''
890+
const maxResponseSize = 50000 // 50KB limit
891+
892+
if (response.sendMessageResponse) {
893+
for await (const chatEvent of response.sendMessageResponse) {
894+
if (chatEvent.assistantResponseEvent?.content) {
895+
responseContent += chatEvent.assistantResponseEvent.content
896+
if (responseContent.length > maxResponseSize) {
897+
this.#features.logging.warn('LLM response exceeded size limit, truncating')
898+
break
899+
}
900+
}
901+
}
902+
}
903+
904+
return responseContent.trim()
905+
} catch (error) {
906+
this.#features.logging.error(`Memory Bank LLM ranking failed: ${error}`)
907+
return '' // Empty string triggers TF-IDF fallback
908+
}
909+
}
910+
)
911+
912+
// Only update prompt if we got a valid comprehensive prompt
913+
if (comprehensivePrompt && comprehensivePrompt.trim().length > 0) {
914+
params.prompt.prompt = comprehensivePrompt
915+
} else {
916+
this.#features.logging.warn('Empty comprehensive prompt received, using original prompt')
917+
params.prompt.prompt = originalPrompt
918+
}
919+
} catch (error) {
920+
this.#features.logging.error(`Memory Bank preparation failed: ${error}`)
921+
// Restore original prompt to ensure no data loss
922+
params.prompt.prompt = originalPrompt
923+
}
924+
}
925+
836926
const maybeDefaultResponse = !params.prompt.command && getDefaultChatResponse(params.prompt.prompt)
837927
if (maybeDefaultResponse) {
838928
return maybeDefaultResponse
@@ -909,7 +999,8 @@ export class AgenticChatController implements ChatHandlers {
909999
const additionalContext = await this.#additionalContextProvider.getAdditionalContext(
9101000
triggerContext,
9111001
params.tabId,
912-
params.context
1002+
params.context,
1003+
params.prompt.prompt
9131004
)
9141005
// Add active file to context list if it's not already there
9151006
const activeFile =

server/aws-lsp-codewhisperer/src/language-server/agenticChat/context/additionalContextProvider.ts

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { ChatDatabase } from '../tools/chatDb/chatDb'
3333
import { ChatMessage, ImageBlock, ImageFormat } from '@amzn/codewhisperer-streaming'
3434
import { getRelativePathWithUri, getRelativePathWithWorkspaceFolder } from '../../workspaceContext/util'
3535
import { isSupportedImageExtension, MAX_IMAGE_CONTEXT_COUNT } from '../../../shared/imageVerification'
36-
import { mergeFileLists } from './contextUtils'
36+
import { MemoryBankController } from './memorybank/memoryBankController'
3737

3838
export const ACTIVE_EDITOR_CONTEXT_ID = 'active-editor'
3939

@@ -151,6 +151,30 @@ export class AdditionalContextProvider {
151151

152152
// Filter rules based on user's rules preferences for current tab
153153
let rulesState = this.chatDb.getRules(tabId) || { folders: {}, rules: {} }
154+
155+
// Ensure memory bank files are active by default when first discovered
156+
const memoryBankFiles = rulesFiles.filter(rule => rule.id?.includes('memory-bank'))
157+
if (memoryBankFiles.length > 0) {
158+
let needsUpdate = false
159+
160+
const memoryBankFolderName = 'memory-bank'
161+
if (rulesState.folders[memoryBankFolderName] === undefined) {
162+
rulesState.folders[memoryBankFolderName] = true
163+
needsUpdate = true
164+
}
165+
166+
memoryBankFiles.forEach(file => {
167+
if (rulesState.rules[file.id] === undefined) {
168+
rulesState.rules[file.id] = true
169+
needsUpdate = true
170+
}
171+
})
172+
173+
if (needsUpdate) {
174+
this.chatDb.setRules(tabId, rulesState)
175+
}
176+
}
177+
154178
return rulesFiles.filter(rule => {
155179
// If the rule has an explicit state in rulesState, use that value
156180
if (rulesState.rules[rule.id] !== undefined) {
@@ -199,7 +223,8 @@ export class AdditionalContextProvider {
199223
async getAdditionalContext(
200224
triggerContext: TriggerContext,
201225
tabId: string,
202-
context?: ContextCommand[]
226+
context?: ContextCommand[],
227+
prompt?: string
203228
): Promise<AdditionalContentEntryAddition[]> {
204229
triggerContext.contextInfo = getInitialContextInfo()
205230

@@ -220,7 +245,33 @@ export class AdditionalContextProvider {
220245
: workspaceUtils.getWorkspaceFolderPaths(this.features.workspace)[0]
221246

222247
if (workspaceRules.length > 0) {
223-
pinnedContextCommands.push(...workspaceRules)
248+
// Check if this is a memory bank generation request
249+
const isMemoryBankRequest = prompt
250+
? new MemoryBankController(this.features).isMemoryBankCreationRequest(prompt)
251+
: false
252+
253+
let rulesToInclude = workspaceRules
254+
255+
if (isMemoryBankRequest) {
256+
// Exclude memory bank files from context when regenerating memory bank
257+
const memoryBankFiles = workspaceRules.filter(rule => rule.id?.includes('memory-bank'))
258+
rulesToInclude = workspaceRules.filter(rule => !rule.id?.includes('memory-bank'))
259+
260+
if (memoryBankFiles.length > 0) {
261+
this.features.logging.info(
262+
`Memory Bank: excluding ${memoryBankFiles.length} existing memory bank files from context`
263+
)
264+
}
265+
} else {
266+
// Normal behavior: include all workspace rules (including memory bank files)
267+
const memoryBankFiles = workspaceRules.filter(rule => rule.id?.includes('memory-bank'))
268+
if (memoryBankFiles.length > 0) {
269+
this.features.logging.info(`Including ${memoryBankFiles.length} memory bank files in chat context`)
270+
}
271+
}
272+
273+
// Add the filtered rules to pinned context
274+
pinnedContextCommands.push(...rulesToInclude)
224275
}
225276

226277
// Merge pinned context with context added to prompt, avoiding duplicates
@@ -675,7 +726,12 @@ export class AdditionalContextProvider {
675726
if (dirPath === '.') {
676727
folderName = undefined
677728
} else {
678-
folderName = dirPath
729+
// Special handling for memory bank files
730+
if (dirPath === '.amazonq/rules/memory-bank') {
731+
folderName = 'memory-bank'
732+
} else {
733+
folderName = dirPath
734+
}
679735
}
680736
} else {
681737
// In multi-workspace: include workspace folder name for all files

0 commit comments

Comments
 (0)