Skip to content

Commit

Permalink
Chat: fix at-mention token size (#3526)
Browse files Browse the repository at this point in the history
  • Loading branch information
abeatrix committed Mar 26, 2024
1 parent 0eccb1d commit d735ff6
Show file tree
Hide file tree
Showing 8 changed files with 74 additions and 25 deletions.
1 change: 1 addition & 0 deletions vscode/CHANGELOG.md
Expand Up @@ -16,6 +16,7 @@ This is a log of all notable changes to Cody for VS Code. [Unreleased] changes a

- Chat: Large file cannot be added via @-mention. [pull/3531](https://github.com/sourcegraph/cody/pull/3531)
- Chat: Handle empty chat message input and prevent submission of empty messages. [pull/3554](https://github.com/sourcegraph/cody/pull/3554)
- Chat: Warnings are now displayed correctly for large files in the @-mention file selection list. [pull/3526](https://github.com/sourcegraph/cody/pull/3526)

### Changed

Expand Down
7 changes: 6 additions & 1 deletion vscode/src/chat/chat-view/SimpleChatModel.ts
Expand Up @@ -4,6 +4,7 @@ import {
type ChatMessage,
type ContextItem,
type Message,
ModelProvider,
type SerializedChatInteraction,
type SerializedChatTranscript,
errorToChatError,
Expand All @@ -15,13 +16,17 @@ import type { Repo } from '../../context/repo-fetcher'
import { getChatPanelTitle } from './chat-helpers'

export class SimpleChatModel {
// The maximum number of characters available in the model's context window.
public readonly maxChars: number
constructor(
public modelID: string,
private messages: ChatMessage[] = [],
public readonly sessionID: string = new Date(Date.now()).toUTCString(),
private customChatTitle?: string,
private selectedRepos?: Repo[]
) {}
) {
this.maxChars = ModelProvider.getMaxCharsByModel(this.modelID)
}

public isEmpty(): boolean {
return this.messages.length === 0
Expand Down
15 changes: 12 additions & 3 deletions vscode/src/chat/chat-view/SimpleChatPanelProvider.ts
Expand Up @@ -46,12 +46,13 @@ import {
} from '../../services/utils/codeblock-action-tracker'
import { openExternalLinks, openLocalFileWithRange } from '../../services/utils/workspace-action'
import { TestSupport } from '../../test-support'
import { countGeneratedCode } from '../utils'
import { countGeneratedCode, getContextWindowLimitInBytes } from '../utils'

import type { Span } from '@opentelemetry/api'
import { captureException } from '@sentry/core'
import type { ContextItemWithContent } from '@sourcegraph/cody-shared/src/codebase-context/messages'
import { ModelUsage } from '@sourcegraph/cody-shared/src/models/types'
import { ANSWER_TOKENS, tokensToChars } from '@sourcegraph/cody-shared/src/prompt/constants'
import { recordErrorToSpan, tracer } from '@sourcegraph/cody-shared/src/tracing'
import type { EnterpriseContextFactory } from '../../context/enterprise-context-factory'
import type { Repo } from '../../context/repo-fetcher'
Expand Down Expand Up @@ -596,12 +597,20 @@ export class SimpleChatPanelProvider implements vscode.Disposable, ChatSession {
})
},
}
// Use the number of characters left in the chat model as the limit
// for adding user context files to the chat.
const contextLimit = getContextWindowLimitInBytes(
[...this.chatModel.getMessages()],
// Minus the character limit reserved for the answer token
this.chatModel.maxChars - tokensToChars(ANSWER_TOKENS)
)

try {
const items = await getChatContextItemsForMention(
query,
cancellation.token,
scopedTelemetryRecorder
scopedTelemetryRecorder,
contextLimit
)
if (cancellation.token.isCancellationRequested) {
return
Expand Down Expand Up @@ -781,7 +790,7 @@ export class SimpleChatPanelProvider implements vscode.Disposable, ChatSession {
): Promise<Message[]> {
const { prompt, newContextUsed, newContextIgnored } = await prompter.makePrompt(
this.chatModel,
ModelProvider.getMaxCharsByModel(this.chatModel.modelID)
this.chatModel.maxChars
)

// Update UI based on prompt construction
Expand Down
8 changes: 5 additions & 3 deletions vscode/src/chat/context/chatContext.ts
Expand Up @@ -19,7 +19,9 @@ export async function getChatContextItemsForMention(
telemetryRecorder?: {
empty: () => void
withType: (type: MentionQuery['type']) => void
}
},
// The number of characters left in current context window.
maxChars?: number
): Promise<ContextItem[]> {
const mentionQuery = parseMentionQuery(query)

Expand All @@ -35,12 +37,12 @@ export async function getChatContextItemsForMention(
const MAX_RESULTS = 20
switch (mentionQuery.type) {
case 'empty':
return getOpenTabsContextFile()
return getOpenTabsContextFile(maxChars)
case 'symbol':
// It would be nice if the VS Code symbols API supports cancellation, but it doesn't
return getSymbolContextFiles(mentionQuery.text, MAX_RESULTS)
case 'file':
return getFileContextFiles(mentionQuery.text, MAX_RESULTS, cancellationToken)
return getFileContextFiles(mentionQuery.text, MAX_RESULTS, maxChars)
case 'url':
return (await isURLContextFeatureFlagEnabled())
? getURLContextItems(
Expand Down
34 changes: 33 additions & 1 deletion vscode/src/chat/utils.ts
@@ -1,4 +1,4 @@
import type { AuthStatus } from '@sourcegraph/cody-shared'
import type { AuthStatus, ChatMessage } from '@sourcegraph/cody-shared'
import { defaultAuthStatus, unauthenticatedStatus } from './protocol'

/**
Expand Down Expand Up @@ -82,3 +82,35 @@ export const countGeneratedCode = (text: string): { lineCount: number; charCount
}
return count
}

/**
* Counts the total number of bytes used in a list of chat messages.
*
* This function is exported and can be used to calculate the byte usage
* of chat messages for storage/bandwidth purposes.
*
* @param messages - The list of chat messages to count bytes for
* @returns The total number of bytes used in the messages
*/
export function countBytesInChatMessages(messages: ChatMessage[]): number {
if (messages.length === 0) {
return 0
}
return messages.reduce((acc, msg) => acc + msg.speaker.length + (msg.text?.length || 0) + 3, 0)
}

/**
* Gets the context window limit in bytes for chat messages, taking into
* account the maximum allowed character count. Returns 0 if the used bytes
* exceeds the limit.
* @param messages - The chat messages
* @param maxChars - The maximum allowed character count
* @returns The context window limit in bytes
*/
export function getContextWindowLimitInBytes(messages: ChatMessage[], maxChars: number): number {
const used = countBytesInChatMessages(messages)
if (used > maxChars) {
return 0
}
return maxChars - used
}
5 changes: 2 additions & 3 deletions vscode/src/edit/input/get-matching-context.ts
@@ -1,6 +1,6 @@
import type { ContextItem, MentionQuery } from '@sourcegraph/cody-shared'
import * as vscode from 'vscode'

import { DEFAULT_FAST_MODEL_CHARS_LIMIT } from '@sourcegraph/cody-shared/src/prompt/constants'
import { getFileContextFiles, getSymbolContextFiles } from '../../editor/utils/editor-context'
import { getLabelForContextItem } from './utils'

Expand Down Expand Up @@ -29,11 +29,10 @@ export async function getMatchingContext(
}

if (mentionQuery.type === 'file') {
const cancellation = new vscode.CancellationTokenSource()
const fileResults = await getFileContextFiles(
mentionQuery.text,
MAX_FUZZY_RESULTS,
cancellation.token
DEFAULT_FAST_MODEL_CHARS_LIMIT
)
return fileResults.map(result => ({
key: getLabelForContextItem(result),
Expand Down
11 changes: 4 additions & 7 deletions vscode/src/editor/utils/editor-context.test.ts
Expand Up @@ -41,11 +41,7 @@ describe('getFileContextFiles', () => {
}

async function runSearch(query: string, maxResults: number): Promise<(string | undefined)[]> {
const results = await getFileContextFiles(
query,
maxResults,
new vscode.CancellationTokenSource().token
)
const results = await getFileContextFiles(query, maxResults)

return results.map(f => uriBasename(f.uri))
}
Expand Down Expand Up @@ -158,13 +154,14 @@ describe('filterLargeFiles', () => {
expect(filtered).toEqual([])
})

it('sets isTooLarge for files exceeding token limit', async () => {
it('sets isTooLarge for files exceeding token limit but under 1MB', async () => {
const largeTextFile: ContextItemFile = {
uri: vscode.Uri.file('/large-text.txt'),
type: 'file',
}
const oneByteOverTokenLimit = MAX_CURRENT_FILE_TOKENS * CHARS_PER_TOKEN + 1
vscode.workspace.fs.stat = vi.fn().mockResolvedValueOnce({
size: MAX_CURRENT_FILE_TOKENS * CHARS_PER_TOKEN + 1,
size: oneByteOverTokenLimit,
type: vscode.FileType.File,
} as vscode.FileStat)

Expand Down
18 changes: 11 additions & 7 deletions vscode/src/editor/utils/editor-context.ts
Expand Up @@ -56,7 +56,7 @@ const throttledFindFiles = throttle(() => findWorkspaceFiles(), 10000)
export async function getFileContextFiles(
query: string,
maxResults: number,
token: vscode.CancellationToken
charsLimit?: number
): Promise<ContextItemFile[]> {
if (!query.trim()) {
return []
Expand Down Expand Up @@ -116,7 +116,7 @@ export async function getFileContextFiles(

// TODO(toolmantim): Add fuzzysort.highlight data to the result so we can show it in the UI

return await filterLargeFiles(sortedResults)
return await filterLargeFiles(sortedResults, charsLimit)
}

export async function getSymbolContextFiles(
Expand Down Expand Up @@ -182,11 +182,12 @@ export async function getSymbolContextFiles(
* Gets context files for each open editor tab in VS Code.
* Filters out large files over 1MB to avoid expensive parsing.
*/
export async function getOpenTabsContextFile(): Promise<ContextItemFile[]> {
export async function getOpenTabsContextFile(charsLimit?: number): Promise<ContextItemFile[]> {
return await filterLargeFiles(
getOpenTabsUris()
.filter(uri => !isCodyIgnoredFile(uri))
.flatMap(uri => createContextFileFromUri(uri, ContextItemSource.User, 'file'))
.flatMap(uri => createContextFileFromUri(uri, ContextItemSource.User, 'file')),
charsLimit
)
}

Expand Down Expand Up @@ -253,7 +254,10 @@ function createContextFileRange(selectionRange: vscode.Range): ContextItem['rang
* Filters the given context files to remove files larger than 1MB and non-text files.
* Sets {@link ContextItemFile.isTooLarge} for files contains more characters than the token limit.
*/
export async function filterLargeFiles(contextFiles: ContextItemFile[]): Promise<ContextItemFile[]> {
export async function filterLargeFiles(
contextFiles: ContextItemFile[],
charsLimit = CHARS_PER_TOKEN * MAX_CURRENT_FILE_TOKENS
): Promise<ContextItemFile[]> {
const filtered = []
for (const cf of contextFiles) {
// Remove file larger than 1MB and non-text files
Expand All @@ -262,13 +266,13 @@ export async function filterLargeFiles(contextFiles: ContextItemFile[]): Promise
stat => stat,
error => undefined
)
if (fileStat?.type !== vscode.FileType.File || fileStat?.size > 1000000) {
if (cf.type !== 'file' || fileStat?.type !== vscode.FileType.File || fileStat?.size > 1000000) {
continue
}
// Check if file contains more characters than the token limit based on fileStat.size
// and set {@link ContextItemFile.isTooLarge} for webview to display file size
// warning.
if (fileStat.size > CHARS_PER_TOKEN * MAX_CURRENT_FILE_TOKENS) {
if (fileStat.size > charsLimit) {
cf.isTooLarge = true
}
filtered.push(cf)
Expand Down

0 comments on commit d735ff6

Please sign in to comment.