mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 14:51:44 +00:00
Cody: Fallback to keyword context for invalid embeddings client (#50952)
RE: https://github.com/sourcegraph/sourcegraph/issues/50876 This PR addresses the issues raised by a customer and beyang ([slack thread](https://sourcegraph.slack.com/archives/C04NPH6SZMW/p1681861031576389?thread_ts=1681859730.152309&channel=C04NPH6SZMW&message_ts=1681861031.576389)) regarding cody.codebase: 1. make it visible to the user what context mechanism is being used 1. we should fall back to keyword search if embeddings don't work 1. also lack of embeddings should surface a user-visible error ### PR Summary #### Issue 1: make it visible to the user what context mechanism is being used Solution from this PR: Show the current fetching method in the lower left corner   #### Issue 2: fall back to keyword search when embedding is not available Solution from this PR: Fallback to keyword search when: - codebase is not provided - embeddings are not available for the current codebase When codebase is not provided and the cody.useContext is set to 'embedding' or 'blended', we will now fallback to use keyword search (see context fetching mode in lower left corner)  When code is provided but Cody is unable to connect to the codebase, we will now fallback to use keyword search (see context fetching mode in lower left corner)  #### Issue 4: lack of embeddings should surface a user-visible error Solution from this PR: display message on codebase change / invalid codebase in UI (see screenshots above) When the cody.codebase is change, a pop up will also show up and ask user to reload VS Code (see lower right corner):  #### other minor fix - Disable chat submission on enter key press when chat is in progress ## Test plan <!-- All pull requests REQUIRE a test plan: https://docs.sourcegraph.com/dev/background-information/testing_principles --> See screenshot shared above. Passed all unit test ``` PASS cody src/configuration.test.ts PASS cody src/completions/context.test.ts PASS cody src/keyword-context/local-keyword-context-fetcher.test.ts Test Suites: 3 passed, 3 total Tests: 5 passed, 5 total Snapshots: 0 total Time: 2.127 s Ran all test suites. ``` --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
541837b9af
commit
df999af89a
@ -1,4 +1,8 @@
|
||||
import { ConfigurationUseContext } from '../configuration'
|
||||
|
||||
export interface ChatContextStatus {
|
||||
mode?: ConfigurationUseContext
|
||||
connection?: boolean
|
||||
codebase?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
@ -26,19 +26,21 @@ export class CodebaseContext {
|
||||
|
||||
public async getContextMessages(query: string, options: ContextSearchOptions): Promise<ContextMessage[]> {
|
||||
switch (this.config.useContext) {
|
||||
case 'blended':
|
||||
case 'embeddings' || 'blended':
|
||||
return this.embeddings
|
||||
? this.getEmbeddingsContextMessages(query, options)
|
||||
: this.getKeywordContextMessages(query, options)
|
||||
case 'embeddings':
|
||||
return this.getEmbeddingsContextMessages(query, options)
|
||||
case 'keyword':
|
||||
return this.getKeywordContextMessages(query, options)
|
||||
default:
|
||||
return []
|
||||
return this.getEmbeddingsContextMessages(query, options)
|
||||
}
|
||||
}
|
||||
|
||||
public checkEmbeddingsConnection(): boolean {
|
||||
return !!this.embeddings
|
||||
}
|
||||
|
||||
// We split the context into multiple messages instead of joining them into a single giant message.
|
||||
// We can gradually eliminate them from the prompt, instead of losing them all at once with a single large messeage
|
||||
// when we run out of tokens.
|
||||
|
||||
@ -106,15 +106,15 @@ export const Chat: React.FunctionComponent<ChatProps> = ({
|
||||
)
|
||||
|
||||
const onChatSubmit = useCallback((): void => {
|
||||
// Submit chat only when input is not empty
|
||||
if (formInput.trim()) {
|
||||
// Submit chat only when input is not empty and not in progress
|
||||
if (formInput.trim() && !messageInProgress) {
|
||||
onSubmit(formInput)
|
||||
setHistoryIndex(inputHistory.length + 1)
|
||||
setInputHistory([...inputHistory, formInput])
|
||||
setInputRows(5)
|
||||
setFormInput('')
|
||||
}
|
||||
}, [formInput, inputHistory, onSubmit, setFormInput, setInputHistory])
|
||||
}, [formInput, inputHistory, messageInProgress, onSubmit, setFormInput, setInputHistory])
|
||||
|
||||
const onChatKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>): void => {
|
||||
|
||||
@ -51,8 +51,7 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(var(--spacing) * 0.5);
|
||||
align-items: center;
|
||||
padding: 0 var(--spacing);
|
||||
}
|
||||
|
||||
@ -61,6 +60,7 @@
|
||||
word-break: break-word;
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
.content pre {
|
||||
padding: calc(var(--spacing) * 0.5);
|
||||
overflow-x: auto;
|
||||
|
||||
@ -92,7 +92,9 @@ export const TranscriptItem: React.FunctionComponent<
|
||||
{message.displayText ? (
|
||||
<CodeBlocks displayText={message.displayText} copyButtonClassName={codeBlocksCopyButtonClassName} />
|
||||
) : inProgress ? (
|
||||
<BlinkingCursor />
|
||||
<span>
|
||||
Fetching context... <BlinkingCursor />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.4rem 0.3rem 0.7rem;
|
||||
font-size: 0.9rem;
|
||||
min-width: calc(100% - 1.1rem);
|
||||
}
|
||||
.container-open {
|
||||
padding-bottom: 0.6rem;
|
||||
@ -16,7 +17,7 @@
|
||||
align-items: center;
|
||||
gap: 0.2rem;
|
||||
cursor: pointer;
|
||||
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
appearance: none;
|
||||
background: none;
|
||||
@ -24,6 +25,7 @@
|
||||
outline: none;
|
||||
color: currentColor;
|
||||
font-family: inherit;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.open-close-icon {
|
||||
opacity: 0.8;
|
||||
|
||||
@ -46,3 +46,12 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fail {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
overflow: hidden;
|
||||
|
||||
color: var(--vscode-errorForeground, red);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react'
|
||||
|
||||
import { mdiFileDocumentOutline, mdiSourceRepository } from '@mdi/js'
|
||||
import { mdiFileDocumentOutline, mdiSourceRepository, mdiFileExcel } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { ChatContextStatus } from '@sourcegraph/cody-shared/src/chat/context'
|
||||
@ -19,9 +19,9 @@ export const ChatInputContext: React.FunctionComponent<{
|
||||
[
|
||||
contextStatus.codebase
|
||||
? {
|
||||
icon: mdiSourceRepository,
|
||||
icon: contextStatus.connection ? mdiSourceRepository : mdiFileExcel,
|
||||
text: basename(contextStatus.codebase.replace(/^(github|gitlab)\.com\//, '')),
|
||||
tooltip: contextStatus.codebase,
|
||||
tooltip: contextStatus.connection ? contextStatus.codebase : 'connection failed',
|
||||
}
|
||||
: null,
|
||||
contextStatus.filePath
|
||||
@ -32,12 +32,14 @@ export const ChatInputContext: React.FunctionComponent<{
|
||||
}
|
||||
: null,
|
||||
].filter(isDefined),
|
||||
[contextStatus.codebase, contextStatus.filePath]
|
||||
[contextStatus.codebase, contextStatus.connection, contextStatus.filePath]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, className)}>
|
||||
<h3 className={styles.badge}>{items.length > 0 ? 'Context' : 'No context'}</h3>
|
||||
<h3 className={styles.badge}>
|
||||
{contextStatus.mode && contextStatus.connection ? 'Embeddings' : 'Keyword'}
|
||||
</h3>
|
||||
{items.length > 0 && (
|
||||
<ul className={styles.items}>
|
||||
{items.map(({ icon, text, tooltip }, index) => (
|
||||
@ -56,7 +58,7 @@ const ContextItem: React.FunctionComponent<{ icon: string; text: string; tooltip
|
||||
tooltip,
|
||||
as: Tag,
|
||||
}) => (
|
||||
<Tag className={styles.item}>
|
||||
<Tag className={tooltip === 'connection failed' ? styles.fail : styles.item}>
|
||||
<Icon svgPath={icon} className={styles.itemIcon} />
|
||||
<span className={styles.itemText} title={tooltip}>
|
||||
{text}
|
||||
|
||||
@ -37,7 +37,7 @@ export async function isValidLogin(
|
||||
|
||||
type Config = Pick<
|
||||
ConfigurationWithAccessToken,
|
||||
'codebase' | 'serverEndpoint' | 'debug' | 'customHeaders' | 'accessToken'
|
||||
'codebase' | 'serverEndpoint' | 'debug' | 'customHeaders' | 'accessToken' | 'useContext'
|
||||
>
|
||||
|
||||
export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disposable {
|
||||
@ -318,6 +318,8 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
void this.webview?.postMessage({
|
||||
type: 'contextStatus',
|
||||
contextStatus: {
|
||||
mode: this.config.useContext,
|
||||
connection: this.codebaseContext.checkEmbeddingsConnection(),
|
||||
codebase: this.config.codebase,
|
||||
filePath: editorContext ? vscode.workspace.asRelativePath(editorContext.filePath) : undefined,
|
||||
},
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { window } from 'vscode'
|
||||
|
||||
import { ChatClient } from '@sourcegraph/cody-shared/src/chat/chat'
|
||||
import { CodebaseContext } from '@sourcegraph/cody-shared/src/codebase-context'
|
||||
import { ConfigurationWithAccessToken } from '@sourcegraph/cody-shared/src/configuration'
|
||||
@ -41,6 +43,7 @@ export async function configureExternalServices(
|
||||
`Cody could not find the '${initialConfig.codebase}' repository on your Sourcegraph instance.\n` +
|
||||
'Please check that the repository exists and is entered correctly in the cody.codebase setting.'
|
||||
console.error(errorMessage)
|
||||
void window.showErrorMessage(errorMessage)
|
||||
}
|
||||
const embeddingsSearch = repoId && !isError(repoId) ? new SourcegraphEmbeddingsSearchClient(client, repoId) : null
|
||||
|
||||
|
||||
@ -54,9 +54,19 @@ export async function start(context: vscode.ExtensionContext): Promise<vscode.Di
|
||||
}
|
||||
}),
|
||||
vscode.workspace.onDidChangeConfiguration(async event => {
|
||||
if (event.affectsConfiguration('cody') || event.affectsConfiguration('sourcegraph')) {
|
||||
if (event.affectsConfiguration('cody')) {
|
||||
onConfigurationChange(await getFullConfig())
|
||||
}
|
||||
if (event.affectsConfiguration('cody.codebase')) {
|
||||
const action = await vscode.window.showInformationMessage(
|
||||
'You must reload VS Code for Cody to pick up your new codebase.',
|
||||
'Reload VS Code',
|
||||
'Close'
|
||||
)
|
||||
if (action === 'Reload VS Code') {
|
||||
void vscode.commands.executeCommand('workbench.action.reloadWindow')
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@ -161,13 +171,6 @@ const register = async (
|
||||
await secretStorage.delete(CODY_ACCESS_TOKEN_SECRET)
|
||||
logEvent('CodyVSCodeExtension:codyDeleteAccessToken:clicked')
|
||||
}),
|
||||
// TOS
|
||||
vscode.commands.registerCommand('cody.accept-tos', version =>
|
||||
localStorage.set('cody.tos-version-accepted', version)
|
||||
),
|
||||
vscode.commands.registerCommand('cody.get-accepted-tos-version', () =>
|
||||
localStorage.get('cody.tos-version-accepted')
|
||||
),
|
||||
// Commands
|
||||
vscode.commands.registerCommand('cody.focus', () => vscode.commands.executeCommand('cody.chat.focus')),
|
||||
vscode.commands.registerCommand('cody.settings', () => chatProvider.setWebviewView('settings')),
|
||||
|
||||
@ -24,4 +24,28 @@ button {
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
flex-direction: row;
|
||||
display: flex;
|
||||
margin: 0.5em;
|
||||
padding: 1rem;
|
||||
color: var(--vscode-input-foreground);
|
||||
background-color: var(--vscode-inputValidation-errorBackground);
|
||||
border: 2px solid var(--vscode-inputValidation-errorBorder);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--vscode-input-foreground);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc
|
||||
const [inputHistory, setInputHistory] = useState<string[] | []>([])
|
||||
const [userHistory, setUserHistory] = useState<ChatHistory | null>(null)
|
||||
const [contextStatus, setContextStatus] = useState<ChatContextStatus | null>(null)
|
||||
const [errorMessage, setErrorMessage] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
vscodeAPI.onMessage(message => {
|
||||
@ -65,6 +66,16 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc
|
||||
break
|
||||
case 'contextStatus':
|
||||
setContextStatus(message.contextStatus)
|
||||
if (message.contextStatus.mode !== 'keyword' && !message.contextStatus?.codebase) {
|
||||
setErrorMessage(
|
||||
'Codebase is missing. A codebase must be provided via the cody.codebase setting to enable embeddings. Failling back to local keyword search for context.'
|
||||
)
|
||||
}
|
||||
if (message.contextStatus?.codebase && !message.contextStatus?.connection) {
|
||||
setErrorMessage(
|
||||
'Codebase connection failed. Please make sure the codebase in your cody.codebase setting is correct and exists in your Sourcegraph instance. Falling back to local keyword search for context.'
|
||||
)
|
||||
}
|
||||
break
|
||||
case 'view':
|
||||
setView(message.messages)
|
||||
@ -102,7 +113,7 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc
|
||||
{view === 'login' && (
|
||||
<Login onLogin={onLogin} isValidLogin={isValidLogin} serverEndpoint={config?.serverEndpoint} />
|
||||
)}
|
||||
{view && view !== 'login' && <NavBar view={view} setView={setView} devMode={Boolean(config?.debug)} />}
|
||||
{view !== 'login' && <NavBar view={view} setView={setView} devMode={Boolean(config?.debug)} />}
|
||||
{view === 'debug' && config?.debug && <Debug debugLog={debugLog} />}
|
||||
{view === 'history' && (
|
||||
<UserHistory
|
||||
@ -116,6 +127,14 @@ export const App: React.FunctionComponent<{ vscodeAPI: VSCodeWrapper }> = ({ vsc
|
||||
{view === 'settings' && (
|
||||
<Settings setView={setView} onLogout={onLogout} serverEndpoint={config?.serverEndpoint} />
|
||||
)}
|
||||
{view === 'chat' && errorMessage && (
|
||||
<div className="error">
|
||||
Error: {errorMessage}
|
||||
<button type="button" onClick={() => setErrorMessage('')} className="close-btn">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{view === 'chat' && (
|
||||
<Chat
|
||||
messageInProgress={messageInProgress}
|
||||
|
||||
@ -38,6 +38,7 @@ body[data-vscode-theme-kind='vscode-high-contrast'] .human-transcript-item {
|
||||
.transcript-action {
|
||||
background: var(--button-secondary-background);
|
||||
color: var(--button-secondary-foreground);
|
||||
font-size: var(--vscode-editor-font-size);
|
||||
}
|
||||
|
||||
.code-blocks-copy-button {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user