Skip to content

Commit 3e1e995

Browse files
authored
feat(inline completion): enhance inline completion supplemental context fetching (#5256)
1 parent 88c7687 commit 3e1e995

File tree

9 files changed

+190
-62
lines changed

9 files changed

+190
-62
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Enhance Q inline completion context fetching for better suggestion quality"
4+
}

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/workspace/context/ProjectContextProviderTest.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer
4141
import software.aws.toolkits.jetbrains.services.amazonq.project.IndexRequest
4242
import software.aws.toolkits.jetbrains.services.amazonq.project.IndexUpdateMode
4343
import software.aws.toolkits.jetbrains.services.amazonq.project.InlineBm25Chunk
44+
import software.aws.toolkits.jetbrains.services.amazonq.project.InlineContextTarget
4445
import software.aws.toolkits.jetbrains.services.amazonq.project.LspMessage
4546
import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider
4647
import software.aws.toolkits.jetbrains.services.amazonq.project.QueryChatRequest
@@ -237,13 +238,13 @@ class ProjectContextProviderTest {
237238
@Test
238239
fun `queryInline should send correct encrypted request to lsp`() = runTest {
239240
sut = ProjectContextProvider(project, encoderServer, this)
240-
sut.queryInline("foo", "Foo.java")
241+
sut.queryInline("foo", "Foo.java", InlineContextTarget.CODEMAP)
241242
advanceUntilIdle()
242243

243-
val request = QueryInlineCompletionRequest("foo", "Foo.java")
244+
val request = QueryInlineCompletionRequest("foo", "Foo.java", "codemap")
244245
val requestJson = mapper.writeValueAsString(request)
245246

246-
assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree("""{ "query": "foo", "filePath": "Foo.java" }"""))
247+
assertThat(mapper.readTree(requestJson)).isEqualTo(mapper.readTree("""{ "query": "foo", "filePath": "Foo.java", "target": "codemap" }"""))
247248

248249
val encryptedRequest = encoderServer.encrypt(requestJson)
249250
wireMock.verify(
@@ -315,7 +316,7 @@ class ProjectContextProviderTest {
315316
)
316317

317318
assertThrows<Exception> {
318-
sut.queryInline("foo", "filepath")
319+
sut.queryInline("foo", "filepath", InlineContextTarget.CODEMAP)
319320
advanceUntilIdle()
320321
}
321322
}
@@ -326,7 +327,7 @@ class ProjectContextProviderTest {
326327
fun `query inline should return deserialized bm25 chunks`() = runTest {
327328
sut = ProjectContextProvider(project, encoderServer, this)
328329
advanceUntilIdle()
329-
val r = sut.queryInline("foo", "filepath")
330+
val r = sut.queryInline("foo", "filepath", InlineContextTarget.CODEMAP)
330331
assertThat(r).hasSize(3)
331332
assertThat(r[0]).isEqualTo(
332333
InlineBm25Chunk(
@@ -374,7 +375,7 @@ class ProjectContextProviderTest {
374375

375376
// it won't throw if it's executed within TestDispatcher context
376377
withContext(getCoroutineBgContext()) {
377-
sut.queryInline("foo", "bar")
378+
sut.queryInline("foo", "bar", InlineContextTarget.CODEMAP)
378379
}
379380

380381
advanceUntilIdle()

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ object CodeWhispererConstants {
177177
const val CHUNK_SIZE = 60
178178
const val NUMBER_OF_LINE_IN_CHUNK = 50
179179
const val NUMBER_OF_CHUNK_TO_FETCH = 3
180+
const val MAX_TOTAL_LENGTH = 20480
180181
}
181182

182183
object Utg {

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt

Lines changed: 102 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import software.aws.toolkits.core.utils.debug
2525
import software.aws.toolkits.core.utils.getLogger
2626
import software.aws.toolkits.core.utils.info
2727
import software.aws.toolkits.core.utils.warn
28-
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
2928
import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextController
3029
import software.aws.toolkits.jetbrains.services.codewhisperer.editor.CodeWhispererEditorUtil
3130
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
@@ -44,6 +43,7 @@ import java.io.DataInput
4443
import java.io.DataOutput
4544
import java.util.Collections
4645
import kotlin.coroutines.coroutineContext
46+
import kotlin.time.measureTimedValue
4747

4848
private val contentRootPathProvider = CopyContentRootPathProvider()
4949

@@ -147,12 +147,20 @@ class DefaultCodeWhispererFileContextProvider(private val project: Project) : Fi
147147
val latency = System.currentTimeMillis() - startFetchingTimestamp
148148
if (it.contents.isNotEmpty()) {
149149
val logStr = buildString {
150-
append("Successfully fetched supplemental context with strategy ${it.strategy} with $latency ms")
150+
append(
151+
"""Q inline completion supplemental context:
152+
| Strategy: ${it.strategy},
153+
| Latency: $latency ms,
154+
| Contents: ${it.contents.size} chunks,
155+
| ContentLength: ${it.contentLength} chars,
156+
| TargetFile: ${it.targetFileName},
157+
""".trimMargin()
158+
)
151159
it.contents.forEachIndexed { index, chunk ->
152160
append(
153161
"""
154162
|
155-
| Chunk ${index + 1}:
163+
| Chunk $index:
156164
| path = ${chunk.path},
157165
| score = ${chunk.score},
158166
| contentLength = ${chunk.content.length}
@@ -219,55 +227,113 @@ class DefaultCodeWhispererFileContextProvider(private val project: Project) : Fi
219227
val query = generateQuery(targetContext)
220228

221229
val contexts = withContext(coroutineContext) {
222-
val projectContextDeferred1 = if (CodeWhispererFeatureConfigService.getInstance().getInlineCompletion()) {
223-
async {
224-
val t0 = System.currentTimeMillis()
225-
val r = fetchProjectContext(query, psiFile, targetContext)
226-
val t1 = System.currentTimeMillis()
227-
LOG.debug {
228-
buildString {
229-
append("time elapse for fetching project context=${t1 - t0}ms; ")
230-
append("numberOfChunks=${r.contents.size}; ")
231-
append("totalLength=${r.contentLength}")
232-
}
230+
val projectContextDeferred1 = async {
231+
val timedCodemapContext = measureTimedValue { fetchProjectContext(query, psiFile, targetContext) }
232+
val codemapContext = timedCodemapContext.value
233+
LOG.debug {
234+
buildString {
235+
append("time elapse for fetching project context=${timedCodemapContext.duration.inWholeMilliseconds}ms; ")
236+
append("numberOfChunks=${codemapContext.contents.size}; ")
237+
append("totalLength=${codemapContext.contentLength}")
233238
}
234-
235-
r
236239
}
237-
} else {
238-
null
240+
241+
codemapContext
239242
}
240243

241244
val openTabsContextDeferred1 = async {
242-
val t0 = System.currentTimeMillis()
243-
val r = fetchOpenTabsContext(query, psiFile, targetContext)
244-
val t1 = System.currentTimeMillis()
245+
val timedOpentabContext = measureTimedValue { fetchOpenTabsContext(query, psiFile, targetContext) }
246+
val opentabContext = timedOpentabContext.value
245247
LOG.debug {
246248
buildString {
247-
append("time elapse for open tabs context=${t1 - t0}ms; ")
248-
append("numberOfChunks=${r.contents.size}; ")
249-
append("totalLength=${r.contentLength}")
249+
append("time elapse for open tabs context=${timedOpentabContext.duration.inWholeMilliseconds}ms; ")
250+
append("numberOfChunks=${opentabContext.contents.size}; ")
251+
append("totalLength=${opentabContext.contentLength}")
250252
}
251253
}
252254

253-
r
255+
opentabContext
254256
}
255257

256-
if (projectContextDeferred1 != null) {
257-
awaitAll(projectContextDeferred1, openTabsContextDeferred1)
258-
} else {
259-
awaitAll(openTabsContextDeferred1)
260-
}
258+
awaitAll(projectContextDeferred1, openTabsContextDeferred1)
261259
}
262260

263-
val projectContext = contexts.find { it.strategy == CrossFileStrategy.ProjectContext }
261+
val projectContext = contexts.find { it.strategy == CrossFileStrategy.Codemap }
264262
val openTabsContext = contexts.find { it.strategy == CrossFileStrategy.OpenTabsBM25 }
265263

266-
return if (projectContext != null && projectContext.contents.isNotEmpty()) {
267-
projectContext
268-
} else {
269-
openTabsContext ?: SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename)
264+
/**
265+
* We're using both codemap and opentabs context
266+
* 1. If both are present, codemap should live in the first of supplemental context list, i.e [codemap, opentabs_0, opentabs_1...] with strategy name codemap
267+
* 2. If only one is present, return the one present with corresponding strategy name, either codemap or opentabs
268+
* 3. If none is present, return empty list with strategy name empty
269+
*
270+
* Service will throw 400 error when context length is greater than 20480, drop the last chunk until the total length fits in the cap
271+
*/
272+
val contextBeforeTruncation = when {
273+
projectContext == null && openTabsContext == null -> SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename)
274+
275+
projectContext != null && openTabsContext != null -> {
276+
val context1 = projectContext.contents
277+
val context2 = openTabsContext.contents
278+
val mergedContext = (context1 + context2).filter { it.content.isNotEmpty() }
279+
280+
val strategy = if (projectContext.contentLength != 0 && openTabsContext.contentLength != 0) {
281+
CrossFileStrategy.Codemap
282+
} else if (projectContext.contentLength != 0) {
283+
CrossFileStrategy.Codemap
284+
} else if (openTabsContext.contentLength != 0) {
285+
CrossFileStrategy.OpenTabsBM25
286+
} else {
287+
CrossFileStrategy.Empty
288+
}
289+
290+
SupplementalContextInfo(
291+
isUtg = false,
292+
contents = mergedContext,
293+
targetFileName = targetContext.filename,
294+
strategy = strategy
295+
)
296+
}
297+
298+
projectContext != null -> {
299+
return if (projectContext.contentLength == 0) {
300+
SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename)
301+
} else {
302+
SupplementalContextInfo(
303+
isUtg = false,
304+
contents = projectContext.contents,
305+
targetFileName = targetContext.filename,
306+
strategy = CrossFileStrategy.Codemap
307+
)
308+
}
309+
}
310+
311+
openTabsContext != null -> {
312+
return if (openTabsContext.contentLength == 0) {
313+
SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename)
314+
} else {
315+
SupplementalContextInfo(
316+
isUtg = false,
317+
contents = openTabsContext.contents,
318+
targetFileName = targetContext.filename,
319+
strategy = CrossFileStrategy.OpenTabsBM25
320+
)
321+
}
322+
}
323+
324+
else -> SupplementalContextInfo.emptyCrossFileContextInfo(targetContext.filename)
325+
}
326+
327+
return truncateContext(contextBeforeTruncation)
328+
}
329+
330+
fun truncateContext(context: SupplementalContextInfo): SupplementalContextInfo {
331+
var c = context.contents
332+
while (c.sumOf { it.content.length } >= CodeWhispererConstants.CrossFile.MAX_TOTAL_LENGTH) {
333+
c = c.dropLast(1)
270334
}
335+
336+
return context.copy(contents = c)
271337
}
272338

273339
@VisibleForTesting
@@ -285,7 +351,7 @@ class DefaultCodeWhispererFileContextProvider(private val project: Project) : Fi
285351
)
286352
},
287353
targetFileName = targetContext.filename,
288-
strategy = CrossFileStrategy.ProjectContext
354+
strategy = CrossFileStrategy.Codemap
289355
)
290356
}
291357

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/SupplementalContextStrategy.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,23 @@ enum class UtgStrategy : SupplementalContextStrategy {
1212
;
1313

1414
override fun toString() = when (this) {
15-
ByName -> "ByName"
16-
ByContent -> "ByContent"
17-
Empty -> "Empty"
15+
ByName -> "byName"
16+
ByContent -> "byContent"
17+
Empty -> "empty"
1818
}
1919
}
2020

2121
enum class CrossFileStrategy : SupplementalContextStrategy {
2222
OpenTabsBM25,
2323
Empty,
2424
ProjectContext,
25+
Codemap,
2526
;
2627

2728
override fun toString() = when (this) {
28-
OpenTabsBM25 -> "OpenTabs_BM25"
29-
Empty -> "Empty"
30-
ProjectContext -> "ProjectContext"
29+
OpenTabsBM25 -> "opentabs"
30+
Empty -> "empty"
31+
ProjectContext -> "projectContext"
32+
Codemap -> "codemap"
3133
}
3234
}

0 commit comments

Comments
 (0)