Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chat: fix at-mention token size #3526

Merged
merged 6 commits into from Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -31,6 +31,7 @@ data class ContextItemFile(
val source: ContextItemSource? = null, // Oneof: embeddings, user, keyword, editor, filename, search, unified, selection, terminal
val type: TypeEnum? = null, // Oneof: file
val isTooLarge: Boolean? = null,
val size: Int? = null,
) : ContextItem() {

enum class TypeEnum {
Expand Down
2 changes: 2 additions & 0 deletions lib/shared/src/codebase-context/messages.ts
Expand Up @@ -84,6 +84,8 @@ export interface ContextItemFile extends ContextItemCommon {
* Whether the file is too large to be included as context.
*/
isTooLarge?: boolean

size?: number
}

/**
Expand Down
17 changes: 16 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,27 @@ import type { Repo } from '../../context/repo-fetcher'
import { getChatPanelTitle } from './chat-helpers'

export class SimpleChatModel {
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 get charsLeft(): number {
let used = 0
for (const msg of this.messages) {
if (used > this.maxChars) {
return 0
}
used += msg.speaker.length + (msg.text?.length || 0) + 3
}
return this.maxChars - used
}

public isEmpty(): boolean {
return this.messages.length === 0
Expand Down
10 changes: 7 additions & 3 deletions vscode/src/chat/chat-view/SimpleChatPanelProvider.ts
Expand Up @@ -52,6 +52,7 @@ 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 } 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 @@ -601,7 +602,8 @@ export class SimpleChatPanelProvider implements vscode.Disposable, ChatSession {
const items = await getChatContextItemsForMention(
query,
cancellation.token,
scopedTelemetryRecorder
scopedTelemetryRecorder,
this.chatModel.charsLeft - ANSWER_TOKENS
)
if (cancellation.token.isCancellationRequested) {
return
Expand Down Expand Up @@ -779,8 +781,10 @@ export class SimpleChatPanelProvider implements vscode.Disposable, ChatSession {
prompter: IPrompter,
sendTelemetry?: (contextSummary: any) => void
): Promise<Message[]> {
const maxChars = ModelProvider.getMaxCharsByModel(this.chatModel.modelID)
const { prompt, newContextUsed } = await prompter.makePrompt(this.chatModel, maxChars)
const { prompt, newContextUsed } = await prompter.makePrompt(
this.chatModel,
this.chatModel.maxChars
)

// Update UI based on prompt construction
this.chatModel.setLastMessageContext(newContextUsed)
Expand Down
7 changes: 4 additions & 3 deletions vscode/src/chat/context/chatContext.ts
Expand Up @@ -19,7 +19,8 @@ export async function getChatContextItemsForMention(
telemetryRecorder?: {
empty: () => void
withType: (type: MentionQuery['type']) => void
}
},
charsLimit?: number
): Promise<ContextItem[]> {
const mentionQuery = parseMentionQuery(query)

Expand All @@ -35,12 +36,12 @@ export async function getChatContextItemsForMention(
const MAX_RESULTS = 20
switch (mentionQuery.type) {
case 'empty':
return getOpenTabsContextFile()
return getOpenTabsContextFile(charsLimit)
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, cancellationToken, charsLimit)
case 'url':
return (await isURLContextFeatureFlagEnabled())
? getURLContextItems(
Expand Down
6 changes: 4 additions & 2 deletions vscode/src/editor/utils/editor-context.test.ts
Expand Up @@ -158,13 +158,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 All @@ -174,6 +175,7 @@ describe('filterLargeFiles', () => {
type: 'file',
uri: largeTextFile.uri,
isTooLarge: true,
size: oneByteOverTokenLimit,
})
})
})
Expand Down
20 changes: 13 additions & 7 deletions vscode/src/editor/utils/editor-context.ts
Expand Up @@ -56,7 +56,8 @@ const throttledFindFiles = throttle(() => findWorkspaceFiles(), 10000)
export async function getFileContextFiles(
query: string,
maxResults: number,
token: vscode.CancellationToken
cancellationToken: vscode.CancellationToken,
charsLimit?: number
): Promise<ContextItemFile[]> {
if (!query.trim()) {
return []
Expand Down Expand Up @@ -116,7 +117,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 +183,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 +255,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 +267,14 @@ 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) {
cf.size = fileStat.size
if (fileStat.size > charsLimit) {
cf.isTooLarge = true
}
filtered.push(cf)
Expand Down