Skip to content

Commit 11bbc79

Browse files
authored
feat(amazonq): Chat Project Context With LSP #5271
Problem: Amazon Q chat is not aware of files opened in user's workspace. User should be able to ask Q any question regarding current open workspace. Solution: Perform local indexing with CodeSage model in LSP. Get the most relevant files for user chat input, and use that input to add project-scoped context when user asks Q any question regarding current open workspace. 1. Download and installation of LSP server, follow DEXP download manifest. We enforce the LSP version in the IDE so that when we release new LSP, there is no production impact. A new extension is required to run new LSP. 2. Start the LSP server with the stdin encryption spec from DEXP. 3. Call the LSP server to index project, 4. Call the LSP server to retrieve project context 5. New settings for the LSP server. Enable/Disable, Threads, Use GPU or not. 6. New metrics.
1 parent c3216f7 commit 11bbc79

File tree

29 files changed

+1207
-19
lines changed

29 files changed

+1207
-19
lines changed

package-lock.json

Lines changed: 12 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "Add support for [Amazon Q Chat Workspace Context](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/workspace-context.html). Customers can use @workspace to ask questions regarding local workspace."
4+
}

packages/amazonq/package.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,30 @@
125125
"markdownDescription": "%AWS.configuration.description.amazonq.shareContentWithAWS%",
126126
"default": true,
127127
"scope": "application"
128+
},
129+
"amazonQ.workspaceIndex": {
130+
"type": "boolean",
131+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndex%",
132+
"default": false,
133+
"scope": "application"
134+
},
135+
"amazonQ.workspaceIndexWorkerThreads": {
136+
"type": "number",
137+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexWorkerThreads%",
138+
"default": 0,
139+
"scope": "application"
140+
},
141+
"amazonQ.workspaceIndexUseGPU": {
142+
"type": "boolean",
143+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexUseGPU%",
144+
"default": false,
145+
"scope": "application"
146+
},
147+
"amazonQ.workspaceIndexMaxSize": {
148+
"type": "number",
149+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexMaxSize%",
150+
"default": 250,
151+
"scope": "application"
128152
}
129153
}
130154
},
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as sinon from 'sinon'
2+
import assert from 'assert'
3+
import { globals } from 'aws-core-vscode/shared'
4+
import { LspClient } from 'aws-core-vscode/amazonq'
5+
6+
describe('Amazon Q LSP client', function () {
7+
let lspClient: LspClient
8+
let encryptFunc: sinon.SinonSpy
9+
10+
beforeEach(async function () {
11+
sinon.stub(globals, 'isWeb').returns(false)
12+
lspClient = new LspClient()
13+
encryptFunc = sinon.spy(lspClient, 'encrypt')
14+
})
15+
16+
it('encrypts payload of query ', async () => {
17+
await lspClient.query('mock_input')
18+
assert.ok(encryptFunc.calledOnce)
19+
assert.ok(encryptFunc.calledWith(JSON.stringify({ query: 'mock_input' })))
20+
const value = await encryptFunc.returnValues[0]
21+
// verifies JWT encryption header
22+
assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`))
23+
})
24+
25+
it('encrypts payload of index files ', async () => {
26+
await lspClient.indexFiles(['fileA'], 'path', false)
27+
assert.ok(encryptFunc.calledOnce)
28+
assert.ok(
29+
encryptFunc.calledWith(
30+
JSON.stringify({
31+
filePaths: ['fileA'],
32+
rootPath: 'path',
33+
refresh: false,
34+
})
35+
)
36+
)
37+
const value = await encryptFunc.returnValues[0]
38+
// verifies JWT encryption header
39+
assert.ok(value.startsWith(`eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0`))
40+
})
41+
42+
it('encrypt removes readable information', async () => {
43+
const sample = 'hello'
44+
const encryptedSample = await lspClient.encrypt(sample)
45+
assert.ok(!encryptedSample.includes('hello'))
46+
})
47+
48+
afterEach(() => {
49+
sinon.restore()
50+
})
51+
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import assert from 'assert'
6+
import sinon from 'sinon'
7+
import { Content, LspController } from 'aws-core-vscode/amazonq'
8+
import { createTestFile } from 'aws-core-vscode/test'
9+
import { fs } from 'aws-core-vscode/shared'
10+
11+
describe('Amazon Q LSP controller', function () {
12+
it('Download mechanism checks against hash, when hash matches', async function () {
13+
const content = {
14+
filename: 'qserver-linux-x64.zip',
15+
url: 'https://x/0.0.6/qserver-linux-x64.zip',
16+
hashes: [
17+
'sha384:768412320f7b0aa5812fce428dc4706b3cae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf17a0a9',
18+
],
19+
bytes: 512,
20+
} as Content
21+
const lspController = new LspController()
22+
sinon.stub(lspController, '_download')
23+
const mockFileName = 'test_case_1.zip'
24+
const mockDownloadFile = await createTestFile(mockFileName)
25+
await fs.writeFile(mockDownloadFile.fsPath, 'test')
26+
const result = await lspController.downloadAndCheckHash(mockDownloadFile.fsPath, content)
27+
assert.strictEqual(result, true)
28+
})
29+
30+
it('Download mechanism checks against hash, when hash does not match', async function () {
31+
const content = {
32+
filename: 'qserver-linux-x64.zip',
33+
url: 'https://x/0.0.6/qserver-linux-x64.zip',
34+
hashes: [
35+
'sha384:38b060a751ac96384cd9327eb1b1e36a21fdb71114be07434c0cc7bf63f6e1da274edebfe76f65fbd51ad2f14898b95b',
36+
],
37+
bytes: 512,
38+
} as Content
39+
const lspController = new LspController()
40+
sinon.stub(lspController, '_download')
41+
const mockFileName = 'test_case_2.zip'
42+
const mockDownloadFile = await createTestFile(mockFileName)
43+
await fs.writeFile(mockDownloadFile.fsPath, 'file_content')
44+
const result = await lspController.downloadAndCheckHash(mockDownloadFile.fsPath, content)
45+
assert.strictEqual(result, false)
46+
})
47+
48+
afterEach(() => {
49+
sinon.restore()
50+
})
51+
})

packages/core/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,30 @@
284284
"markdownDescription": "%AWS.configuration.description.amazonq.shareContentWithAWS%",
285285
"default": true,
286286
"scope": "application"
287+
},
288+
"amazonQ.workspaceIndex": {
289+
"type": "boolean",
290+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndex%",
291+
"default": false,
292+
"scope": "application"
293+
},
294+
"amazonQ.workspaceIndexWorkerThreads": {
295+
"type": "number",
296+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexWorkerThreads%",
297+
"default": 0,
298+
"scope": "application"
299+
},
300+
"amazonQ.workspaceIndexUseGPU": {
301+
"type": "boolean",
302+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexUseGPU%",
303+
"default": false,
304+
"scope": "application"
305+
},
306+
"amazonQ.workspaceIndexMaxSize": {
307+
"type": "number",
308+
"markdownDescription": "%AWS.configuration.description.amazonq.workspaceIndexMaxSize%",
309+
"default": 250,
310+
"scope": "application"
287311
}
288312
}
289313
},
@@ -4118,6 +4142,7 @@
41184142
"immutable": "^4.3.0",
41194143
"js-yaml": "^4.1.0",
41204144
"jsonc-parser": "^3.2.0",
4145+
"jose": "5.4.1",
41214146
"lodash": "^4.17.21",
41224147
"markdown-it": "^13.0.2",
41234148
"mime-types": "^2.1.32",

packages/core/package.nls.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@
7373
"AWS.configuration.description.amazonq": "Amazon Q creates a code reference when you insert a code suggestion from Amazon Q that is similar to training data. When unchecked, Amazon Q will not show code suggestions that have code references. If you authenticate through IAM Identity Center, this setting is controlled by your Amazon Q administrator. [Learn More](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/code-reference.html)",
7474
"AWS.configuration.description.amazonq.shareContentWithAWS": "When checked, your content processed by Amazon Q may be used for service improvement (except for content processed by the Amazon Q Enterprise service tier). Unchecking this box will cause AWS to delete any of your content used for that purpose. The information used to provide the Amazon Q service to you will not be affected. See the [Service Terms](https://aws.amazon.com/service-terms) for more detail.",
7575
"AWS.configuration.description.amazonq.importRecommendation": "Amazon Q will add import statements with inline code suggestions when necessary.",
76+
"AWS.configuration.description.amazonq.workspaceIndex": "This feature is in BETA. When you add @workspace to your question in Amazon Q chat, Amazon Q will index your open workspace files locally to use as context for its response. Extra CPU usage is expected while indexing a workspace. This will not impact Amazon Q features or your IDE, but you may manage CPU usage by setting the number of local threads in 'Local Workspace Index Threads'.",
77+
"AWS.configuration.description.amazonq.workspaceIndexWorkerThreads": "Number of worker threads of Amazon Q local index process. '0' will use the system default worker threads for balance performance. You may increase this number to more quickly index your workspace, but only up to your hardware's number of CPU cores. Please restart VS Code or reload the VS Code window after changing worker threads.",
78+
"AWS.configuration.description.amazonq.workspaceIndexUseGPU": "Enable GPU to help index your local workspace files. Only applies to Linux and Windows.",
79+
"AWS.configuration.description.amazonq.workspaceIndexMaxSize": "The maximum size of local workspace files to be indexed in MB",
7680
"AWS.command.apig.copyUrl": "Copy URL",
7781
"AWS.command.apig.invokeRemoteRestApi": "Invoke on AWS",
7882
"AWS.command.apig.invokeRemoteRestApi.cn": "Invoke on Amazon",

packages/core/src/amazonq/activation.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import { listCodeWhispererCommandsWalkthrough } from '../codewhisperer/ui/status
2222
import { Commands, placeholder } from '../shared/vscode/commands2'
2323
import { focusAmazonQPanel, focusAmazonQPanelKeybinding } from '../codewhispererChat/commands/registerCommands'
2424
import { TryChatCodeLensProvider, tryChatCodeLensCommand } from '../codewhispererChat/editor/codelens'
25+
import { LspController } from './lsp/lspController'
2526
import { Auth } from '../auth'
2627
import { telemetry } from '../shared/telemetry'
28+
import { CodeWhispererSettings } from '../codewhisperer/util/codewhispererSettings'
29+
import { debounce } from '../shared/utilities/functionUtils'
2730

2831
export async function activate(context: ExtensionContext) {
2932
const appInitContext = DefaultAmazonQAppInitContext.instance
@@ -39,6 +42,10 @@ export async function activate(context: ExtensionContext) {
3942

4043
await TryChatCodeLensProvider.register(appInitContext.onDidChangeAmazonQVisibility.event)
4144

45+
const setupLsp = debounce(async () => {
46+
void LspController.instance.trySetupLsp(context)
47+
}, 5000)
48+
4249
context.subscriptions.push(
4350
window.registerWebviewViewProvider(AmazonQChatViewProvider.viewType, provider, {
4451
webviewOptions: {
@@ -52,14 +59,22 @@ export async function activate(context: ExtensionContext) {
5259
listCodeWhispererCommandsWalkthrough.register(),
5360
focusAmazonQPanel.register(),
5461
focusAmazonQPanelKeybinding.register(),
55-
tryChatCodeLensCommand.register()
62+
tryChatCodeLensCommand.register(),
63+
vscode.workspace.onDidChangeConfiguration(async configurationChangeEvent => {
64+
if (configurationChangeEvent.affectsConfiguration('amazonQ.workspaceIndex')) {
65+
if (CodeWhispererSettings.instance.isLocalIndexEnabled()) {
66+
void setupLsp()
67+
}
68+
}
69+
})
5670
)
5771

5872
Commands.register('aws.amazonq.learnMore', () => {
5973
void vscode.env.openExternal(vscode.Uri.parse(amazonQHelpUrl))
6074
})
6175

6276
await activateBadge()
77+
void setupLsp()
6378
void setupAuthNotification()
6479
}
6580

packages/core/src/amazonq/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export { MessageListener } from './messages/messageListener'
1111
export { AuthController } from './auth/controller'
1212
export { showAmazonQWalkthroughOnce } from './onboardingPage/walkthrough'
1313
export { openAmazonQWalkthrough } from './onboardingPage/walkthrough'
14-
14+
export { LspController, Content } from './lsp/lspController'
15+
export { LspClient } from './lsp/lspClient'
1516
/**
1617
* main from createMynahUI is a purely browser dependency. Due to this
1718
* we need to create a wrapper function that will dynamically execute it

0 commit comments

Comments
 (0)