Skip to content

Commit 127a7ff

Browse files
authored
config(amazonq): inline project context AB #5994
## Problem update inline suggestion project context ab test config #### CONTROL - use bm25 opentabs context #### TREATMENT_1 - use repomap + bm25 open tabs context #### TREATMENT_2 - use bm25 global project context
1 parent 4531668 commit 127a7ff

File tree

9 files changed

+290
-38
lines changed

9 files changed

+290
-38
lines changed

packages/amazonq/test/unit/codewhisperer/util/crossFileContextUtil.test.ts

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
} from 'aws-core-vscode/test'
2020
import { areEqual, normalize } from 'aws-core-vscode/shared'
2121
import * as path from 'path'
22+
import { LspController } from 'aws-core-vscode/amazonq'
2223

2324
let tempFolder: string
2425

@@ -39,8 +40,8 @@ describe('crossFileContextUtil', function () {
3940
tempFolder = (await createTestWorkspaceFolder()).uri.fsPath
4041
})
4142

42-
it('opentabs context should fetch 3 chunks and each chunk should contains 50 lines', async function () {
43-
sinon.stub(FeatureConfigProvider.instance, 'isNewProjectContextGroup').alwaysReturned(false)
43+
it('for control group, should return opentabs context where there will be 3 chunks and each chunk should contains 50 lines', async function () {
44+
sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').alwaysReturned('control')
4445
await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false })
4546
const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, {
4647
preview: false,
@@ -53,6 +54,110 @@ describe('crossFileContextUtil', function () {
5354
assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50)
5455
assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50)
5556
})
57+
58+
it('for t1 group, should return repomap + opentabs context', async function () {
59+
await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false })
60+
const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, {
61+
preview: false,
62+
})
63+
sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t1')
64+
sinon
65+
.stub(LspController.instance, 'queryInlineProjectContext')
66+
.withArgs(sinon.match.any, sinon.match.any, 'codemap')
67+
.resolves([
68+
{
69+
content: 'foo',
70+
score: 0,
71+
filePath: 'q-inline',
72+
},
73+
])
74+
75+
const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken)
76+
assert.ok(actual)
77+
assert.ok(actual.supplementalContextItems.length === 4)
78+
assert.strictEqual(actual?.strategy, 'codemap')
79+
assert.deepEqual(actual?.supplementalContextItems[0], {
80+
content: 'foo',
81+
score: 0,
82+
filePath: 'q-inline',
83+
})
84+
85+
assert.strictEqual(actual.supplementalContextItems[1].content.split('\n').length, 50)
86+
assert.strictEqual(actual.supplementalContextItems[2].content.split('\n').length, 50)
87+
assert.strictEqual(actual.supplementalContextItems[3].content.split('\n').length, 50)
88+
})
89+
90+
it('for t2 group, should return global bm25 context and no repomap', async function () {
91+
await toTextEditor(aStringWithLineCount(200), 'CrossFile.java', tempFolder, { preview: false })
92+
const myCurrentEditor = await toTextEditor('', 'TargetFile.java', tempFolder, {
93+
preview: false,
94+
})
95+
sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').returns('t2')
96+
sinon
97+
.stub(LspController.instance, 'queryInlineProjectContext')
98+
.withArgs(sinon.match.any, sinon.match.any, 'bm25')
99+
.resolves([
100+
{
101+
content: 'foo',
102+
score: 5,
103+
filePath: 'foo.java',
104+
},
105+
{
106+
content: 'bar',
107+
score: 4,
108+
filePath: 'bar.java',
109+
},
110+
{
111+
content: 'baz',
112+
score: 3,
113+
filePath: 'baz.java',
114+
},
115+
{
116+
content: 'qux',
117+
score: 2,
118+
filePath: 'qux.java',
119+
},
120+
{
121+
content: 'quux',
122+
score: 1,
123+
filePath: 'quux.java',
124+
},
125+
])
126+
127+
const actual = await crossFile.fetchSupplementalContextForSrc(myCurrentEditor, fakeCancellationToken)
128+
assert.ok(actual)
129+
assert.ok(actual.supplementalContextItems.length === 5)
130+
assert.strictEqual(actual?.strategy, 'bm25')
131+
132+
assert.deepEqual(actual?.supplementalContextItems[0], {
133+
content: 'foo',
134+
score: 5,
135+
filePath: 'foo.java',
136+
})
137+
138+
assert.deepEqual(actual?.supplementalContextItems[1], {
139+
content: 'bar',
140+
score: 4,
141+
filePath: 'bar.java',
142+
})
143+
assert.deepEqual(actual?.supplementalContextItems[2], {
144+
content: 'baz',
145+
score: 3,
146+
filePath: 'baz.java',
147+
})
148+
149+
assert.deepEqual(actual?.supplementalContextItems[3], {
150+
content: 'qux',
151+
score: 2,
152+
filePath: 'qux.java',
153+
})
154+
155+
assert.deepEqual(actual?.supplementalContextItems[4], {
156+
content: 'quux',
157+
score: 1,
158+
filePath: 'quux.java',
159+
})
160+
})
56161
})
57162

58163
describe('non supported language should return undefined', function () {
@@ -212,7 +317,7 @@ describe('crossFileContextUtil', function () {
212317

213318
fileExtLists.forEach((fileExt) => {
214319
it('should be non empty', async function () {
215-
sinon.stub(FeatureConfigProvider.instance, 'isNewProjectContextGroup').alwaysReturned(false)
320+
sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').alwaysReturned('control')
216321
const editor = await toTextEditor('content-1', `file-1.${fileExt}`, tempFolder)
217322
await toTextEditor('content-2', `file-2.${fileExt}`, tempFolder, { preview: false })
218323
await toTextEditor('content-3', `file-3.${fileExt}`, tempFolder, { preview: false })

packages/amazonq/test/unit/codewhisperer/util/supplemetalContextUtil.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ describe('supplementalContextUtil', function () {
2121

2222
beforeEach(async function () {
2323
testFolder = await TestFolder.create()
24-
sinon.stub(FeatureConfigProvider.instance, 'isNewProjectContextGroup').alwaysReturned(false)
24+
sinon.stub(FeatureConfigProvider.instance, 'getProjectContextGroup').alwaysReturned('control')
2525
})
2626

2727
afterEach(function () {

packages/core/src/amazonq/lsp/lspClient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ export class LspClient {
103103
}
104104
}
105105

106-
async queryInlineProjectContext(query: string, path: string) {
106+
async queryInlineProjectContext(query: string, path: string, target: 'default' | 'codemap' | 'bm25') {
107107
try {
108108
const request = JSON.stringify({
109109
query: query,
110110
filePath: path,
111+
target,
111112
})
112113
const encrypted = await this.encrypt(request)
113114
const resp: any = await this.client?.sendRequest(QueryInlineProjectContextRequestType, encrypted)

packages/core/src/amazonq/lsp/lspController.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export interface Manifest {
6666
}
6767
const manifestUrl = 'https://aws-toolkit-language-servers.amazonaws.com/q-context/manifest.json'
6868
// this LSP client in Q extension is only going to work with these LSP server versions
69-
const supportedLspServerVersions = ['0.1.27']
69+
const supportedLspServerVersions = ['0.1.28']
7070

7171
const nodeBinName = process.platform === 'win32' ? 'node.exe' : 'node'
7272

@@ -308,10 +308,13 @@ export class LspController {
308308
return resp
309309
}
310310

311-
async queryInlineProjectContext(query: string, path: string) {
311+
async queryInlineProjectContext(query: string, path: string, target: 'bm25' | 'codemap' | 'default') {
312312
try {
313-
return await LspClient.instance.queryInlineProjectContext(query, path)
313+
return await LspClient.instance.queryInlineProjectContext(query, path, target)
314314
} catch (e) {
315+
if (e instanceof Error) {
316+
getLogger().error(`unexpected error while querying inline project context, e=${e.message}`)
317+
}
315318
return []
316319
}
317320
}

packages/core/src/amazonq/lsp/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export type QueryInlineProjectContextRequest = string
5151
export type QueryInlineProjectContextRequestPayload = {
5252
query: string
5353
filePath: string
54+
target: string
5455
}
5556
export const QueryInlineProjectContextRequestType: RequestType<QueryInlineProjectContextRequest, any, any> =
5657
new RequestType('lsp/queryInlineProjectContext')

packages/core/src/codewhisperer/models/model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ export const vsCodeState: VsCodeState = {
4747

4848
export type UtgStrategy = 'ByName' | 'ByContent'
4949

50-
export type CrossFileStrategy = 'OpenTabs_BM25'
50+
export type CrossFileStrategy = 'opentabs' | 'codemap' | 'bm25' | 'default'
5151

52-
export type SupplementalContextStrategy = CrossFileStrategy | UtgStrategy | 'Empty' | 'LSP'
52+
export type SupplementalContextStrategy = CrossFileStrategy | UtgStrategy | 'Empty'
5353

5454
export interface CodeWhispererSupplementalContext {
5555
isUtg: boolean

packages/core/src/codewhisperer/util/supplementalContext/crossFileContextUtil.ts

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -53,54 +53,137 @@ interface Chunk {
5353
score?: number
5454
}
5555

56-
type SupplementalContextConfig = 'none' | 'v1' | 'v2'
56+
/**
57+
* `none`: supplementalContext is not supported
58+
* `opentabs`: opentabs_BM25
59+
* `codemap`: repomap + opentabs BM25
60+
* `bm25`: global_BM25
61+
* `default`: repomap + global_BM25
62+
*/
63+
type SupplementalContextConfig = 'none' | 'opentabs' | 'codemap' | 'bm25' | 'default'
5764

5865
export async function fetchSupplementalContextForSrc(
5966
editor: vscode.TextEditor,
6067
cancellationToken: vscode.CancellationToken
6168
): Promise<Pick<CodeWhispererSupplementalContext, 'supplementalContextItems' | 'strategy'> | undefined> {
6269
const supplementalContextConfig = getSupplementalContextConfig(editor.document.languageId)
6370

71+
// not supported case
6472
if (supplementalContextConfig === 'none') {
6573
return undefined
6674
}
67-
if (supplementalContextConfig === 'v1') {
68-
return fetchSupplementalContextForSrcV1(editor, cancellationToken)
75+
76+
// opentabs context will use bm25 and users' open tabs to fetch supplemental context
77+
if (supplementalContextConfig === 'opentabs') {
78+
return {
79+
supplementalContextItems: (await fetchOpentabsContext(editor, cancellationToken)) ?? [],
80+
strategy: 'opentabs',
81+
}
82+
}
83+
84+
// codemap will use opentabs context plus repomap if it's present
85+
if (supplementalContextConfig === 'codemap') {
86+
const opentabsContextAndCodemap = await waitUntil(
87+
async function () {
88+
const result: CodeWhispererSupplementalContextItem[] = []
89+
const opentabsContext = await fetchOpentabsContext(editor, cancellationToken)
90+
const codemap = await fetchProjectContext(editor, 'codemap')
91+
92+
if (codemap && codemap.length > 0) {
93+
result.push(...codemap)
94+
}
95+
96+
if (opentabsContext && opentabsContext.length > 0) {
97+
result.push(...opentabsContext)
98+
}
99+
100+
return result
101+
},
102+
{ timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false }
103+
)
104+
105+
return {
106+
supplementalContextItems: opentabsContextAndCodemap ?? [],
107+
strategy: 'codemap',
108+
}
69109
}
70-
const promiseV1 = waitUntil(
110+
111+
// fallback to opentabs if projectContext timeout for 'default' | 'bm25'
112+
const opentabsContextPromise = waitUntil(
71113
async function () {
72-
return await fetchSupplementalContextForSrcV1(editor, cancellationToken)
114+
return await fetchOpentabsContext(editor, cancellationToken)
73115
},
74116
{ timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false }
75117
)
76-
const promiseV2 = waitUntil(
118+
119+
// global bm25 without repomap
120+
if (supplementalContextConfig === 'bm25') {
121+
const projectBM25Promise = waitUntil(
122+
async function () {
123+
return await fetchProjectContext(editor, 'bm25')
124+
},
125+
{ timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false }
126+
)
127+
128+
const [projectContext, opentabsContext] = await Promise.all([projectBM25Promise, opentabsContextPromise])
129+
if (projectContext && projectContext.length > 0) {
130+
return {
131+
supplementalContextItems: projectContext,
132+
strategy: 'bm25',
133+
}
134+
}
135+
136+
return {
137+
supplementalContextItems: opentabsContext ?? [],
138+
strategy: 'opentabs',
139+
}
140+
}
141+
142+
// global bm25 with repomap
143+
const projectContextAndCodemapPromise = waitUntil(
77144
async function () {
78-
return await fetchSupplementalContextForSrcV2(editor)
145+
return await fetchProjectContext(editor, 'default')
79146
},
80147
{ timeout: supplementalContextTimeoutInMs, interval: 5, truthy: false }
81148
)
82-
const [resultV1, resultV2] = await Promise.all([promiseV1, promiseV2])
83-
return resultV2 ?? resultV1
149+
150+
const [projectContext, opentabsContext] = await Promise.all([
151+
projectContextAndCodemapPromise,
152+
opentabsContextPromise,
153+
])
154+
if (projectContext && projectContext.length > 0) {
155+
return {
156+
supplementalContextItems: projectContext,
157+
strategy: 'default',
158+
}
159+
}
160+
161+
return {
162+
supplementalContextItems: opentabsContext ?? [],
163+
strategy: 'opentabs',
164+
}
84165
}
85166

86-
export async function fetchSupplementalContextForSrcV2(
87-
editor: vscode.TextEditor
88-
): Promise<Pick<CodeWhispererSupplementalContext, 'supplementalContextItems' | 'strategy'> | undefined> {
167+
export async function fetchProjectContext(
168+
editor: vscode.TextEditor,
169+
target: 'default' | 'codemap' | 'bm25'
170+
): Promise<CodeWhispererSupplementalContextItem[]> {
89171
const inputChunkContent = getInputChunk(editor)
90172

91173
const inlineProjectContext: { content: string; score: number; filePath: string }[] =
92-
await LspController.instance.queryInlineProjectContext(inputChunkContent.content, editor.document.uri.fsPath)
174+
await LspController.instance.queryInlineProjectContext(
175+
inputChunkContent.content,
176+
editor.document.uri.fsPath,
177+
target
178+
)
93179

94-
return {
95-
supplementalContextItems: [...inlineProjectContext],
96-
strategy: 'LSP',
97-
}
180+
return inlineProjectContext
98181
}
99182

100-
export async function fetchSupplementalContextForSrcV1(
183+
export async function fetchOpentabsContext(
101184
editor: vscode.TextEditor,
102185
cancellationToken: vscode.CancellationToken
103-
): Promise<Pick<CodeWhispererSupplementalContext, 'supplementalContextItems' | 'strategy'> | undefined> {
186+
): Promise<CodeWhispererSupplementalContextItem[] | undefined> {
104187
const codeChunksCalculated = crossFileContextConfig.numberOfChunkToFetch
105188

106189
// Step 1: Get relevant cross files to refer
@@ -151,10 +234,7 @@ export async function fetchSupplementalContextForSrcV1(
151234

152235
// DO NOT send code chunk with empty content
153236
getLogger().debug(`CodeWhisperer finished fetching crossfile context out of ${relevantCrossFilePaths.length} files`)
154-
return {
155-
supplementalContextItems: supplementalContexts,
156-
strategy: 'OpenTabs_BM25',
157-
}
237+
return supplementalContexts
158238
}
159239

160240
function findBestKChunkMatches(chunkInput: Chunk, chunkReferences: Chunk[], k: number): Chunk[] {
@@ -201,10 +281,18 @@ function getSupplementalContextConfig(languageId: vscode.TextDocument['languageI
201281
if (!isCrossFileSupported(languageId)) {
202282
return 'none'
203283
}
204-
if (FeatureConfigProvider.instance.isNewProjectContextGroup()) {
205-
return 'v2'
284+
285+
const group = FeatureConfigProvider.instance.getProjectContextGroup()
286+
switch (group) {
287+
case 'control':
288+
return 'opentabs'
289+
290+
case 't1':
291+
return 'codemap'
292+
293+
case 't2':
294+
return 'bm25'
206295
}
207-
return 'v1'
208296
}
209297

210298
/**

0 commit comments

Comments
 (0)