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

Bee/smart apply i #7103

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions lib/shared/src/chat/preamble.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import type { ChatModel, EditModel } from '../models/types'
import { type PromptString, ps } from '../prompt/prompt-string'
import { SMART_APPLY_SYSTEM_PROMPT } from '../prompt/smart-apply'
import type { Message } from '../sourcegraph-api'

const DEFAULT_PREAMBLE = ps`You are Cody, an AI coding assistant from Sourcegraph.`

/**
* For chat, we add an additional preamble to encourage the model to
* produce code blocks that we can associate executable commands or content with existing file paths.
* We want to read these file paths to support applying code directly to files from chat for Smart Apply.
*/
const SMART_APPLY_PREAMBLE = ps`If your answer contains fenced code blocks in Markdown, include the relevant full file path in the code block tag using this structure: \`\`\`$LANGUAGE:$FILEPATH\`\`\`
For executable terminal commands: enclose each command in individual "bash" language code block without comments and new lines inside.`

const CHAT_PREAMBLE = DEFAULT_PREAMBLE.concat(SMART_APPLY_PREAMBLE)
const CHAT_PREAMBLE = DEFAULT_PREAMBLE.concat(SMART_APPLY_SYSTEM_PROMPT)

export function getSimplePreamble(
model: ChatModel | EditModel | undefined,
Expand Down
14 changes: 10 additions & 4 deletions lib/shared/src/prompt/prompt-mixin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ChatMessage } from '../chat/transcript/messages'
import type { ChatModel } from '../models/types'
import { PromptString, ps } from './prompt-string'
import { SMART_APPLY_SYSTEM_PROMPT } from './smart-apply'

/**
* The preamble we add to the start of the last human open-end chat message that has context items.
Expand Down Expand Up @@ -32,6 +33,11 @@ export class PromptMixin {
*/
private static mixins: PromptMixin[] = []
private static hedging: PromptMixin = new PromptMixin(HEDGES_PREVENTION)
/**
* Instructions on specific output formatting for Smart Apply.
* Only applies to non-Claude-3 models as the same prompt is added to the system prompt for Claude-3 models.
*/
private static smartApply = new PromptMixin(SMART_APPLY_SYSTEM_PROMPT)

/**
* Prepends all mixins to `humanMessage`. Modifies and returns `humanMessage`.
Expand All @@ -48,17 +54,17 @@ export class PromptMixin {
const apologiticModels = ['3-5-sonnet', '3.5-sonnet']
if (modelID && apologiticModels.some(model => modelID.includes(model))) {
mixins.push(PromptMixin.hedging)
}

// Handle agent-specific prompts
if (
} else if (
// Handle agent-specific prompts
humanMessage.agent === 'deep-cody' &&
!newMixins.length &&
!agenticBlockedModels.some(m => modelID?.includes(m))
) {
// Adding hedging prevention prompt for Deep Cody as it's now pined to Sonnet.
mixins.push(PromptMixin.hedging)
mixins.push(new PromptMixin(AGENTIC_CHAT))
} else {
mixins.push(PromptMixin.smartApply)
}

// Add new mixins to the list of mixins to be prepended to the next human message.
Expand Down
18 changes: 18 additions & 0 deletions lib/shared/src/prompt/smart-apply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ps } from './prompt-string'

/**
* For chat, we add an additional preamble to encourage the model to
* produce code blocks that we can associate executable commands or content with existing file paths.
* We want to read these file paths to support applying code directly to files from chat for Smart Apply.
* We also ask for regex patterns where the code block content should be replaced in a file.
*/
export const SMART_APPLY_SYSTEM_PROMPT = ps`If your answer contains fenced code blocks in Markdown, include the relevant full file path in the code block tag using this structure:
\`\`\`$LANGUAGE:$FILEPATH regex:$PATTERN
{CODE}
\`\`\`
The regex pattern must precisely match the modified code enclosed in each code block. The code block content will replace the matched code so ONLY includes regex pattern for replacable code in a code block:
-Capture from the function declaration to end of file: regex:(functionName[\s\S]*$)
-Capture the entire file: regex:.*
-Literal match: regex:stringToMatch
When showing code context, put code outside the replacement area in separate code blocks.
For executable commands, enclose each command in individual "bash" language code block without comments and new lines inside. `
3 changes: 2 additions & 1 deletion vscode/src/chat/chat-view/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,8 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
currentAuthStatus(),
message.instruction,
message.fileName,
message.traceparent
message.traceparent,
message.regex
)
break
case 'trace-export':
Expand Down
1 change: 1 addition & 0 deletions vscode/src/chat/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export type WebviewMessage =
instruction?: string | undefined | null
fileName?: string | undefined | null
traceparent?: string | undefined | null
regex?: string | undefined | null
}
| {
command: 'trace-export'
Expand Down
29 changes: 28 additions & 1 deletion vscode/src/edit/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,11 +273,38 @@
return
}

const document = configuration.document
const { document, replacement, range } = configuration
if (await isUriIgnoredByContextFilterWithNotification(document.uri, 'edit')) {
return
}

// For the cases where a replacement with range has been provided, we can apply the edit directly.
if (replacement && range) {
// Create a task that covers the entire document
const fullDocRange = new vscode.Range(
document.positionAt(0),
document.positionAt(document.getText().length)
)
// Create a task with the regex replacement as the instruction
const task = await this.options.controller.createTask(
document,
ps``,
[],
fullDocRange,
'edit',
'edit',
'manual',
'chat',
document.uri
)
// If the task has failed, we would fallback to smart apply.

Check failure on line 300 in vscode/src/edit/manager.ts

View workflow job for this annotation

GitHub Actions / JetBrains tests

Argument of type 'string' is not assignable to parameter of type 'Rule[]'.

Check failure on line 300 in vscode/src/edit/manager.ts

View workflow job for this annotation

GitHub Actions / build

Argument of type 'string' is not assignable to parameter of type 'Rule[]'.

Check failure on line 300 in vscode/src/edit/manager.ts

View workflow job for this annotation

GitHub Actions / test-unit (ubuntu, 20)

Argument of type 'string' is not assignable to parameter of type 'Rule[]'.

Check failure on line 300 in vscode/src/edit/manager.ts

View workflow job for this annotation

GitHub Actions / test-unit (windows, 20)

Argument of type 'string' is not assignable to parameter of type 'Rule[]'.

Check failure on line 300 in vscode/src/edit/manager.ts

View workflow job for this annotation

GitHub Actions / test-unit (ubuntu, 18)

Argument of type 'string' is not assignable to parameter of type 'Rule[]'.
if (task) {
const provider = this.getProviderForTask(task)
await provider.applyEdit(replacement)
return task
}
}

const model =
configuration.model ||
(await firstResultFromOperation(modelsService.getDefaultEditModel()))
Expand Down
1 change: 1 addition & 0 deletions vscode/src/edit/smart-apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface SmartApplyArguments {
model?: EditModel
isNewFile?: boolean
traceparent: string | undefined | null
range?: vscode.Range | undefined | null
}
source?: EventSource
}
Expand Down
31 changes: 30 additions & 1 deletion vscode/src/services/utils/codeblock-action-tracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
featureFlagProvider,
firstValueFrom,
isDotCom,
ps,
skipPendingOperation,
switchMap,
telemetryRecorder,
Expand Down Expand Up @@ -192,7 +193,8 @@ export async function handleSmartApply(
authStatus: AuthStatus,
instruction?: string | null,
fileUri?: string | null,
traceparent?: string | undefined | null
traceparent?: string | undefined | null,
regex?: string | null
): Promise<void> {
const activeEditor = getEditor()?.active
const workspaceUri = vscode.workspace.workspaceFolders?.[0].uri
Expand All @@ -210,6 +212,33 @@ export async function handleSmartApply(
throw new Error('No editor found to insert text')
}

if (regex) {
// First, check if the regex is actually a literal match
const currentCode = document.getText()
const isLiteralMatch = currentCode.includes(regex)
const regexMatch = new RegExp(regex, 's')
const replacement = currentCode.replace(isLiteralMatch ? regex : regexMatch, code)
if (currentCode !== replacement) {
const range = new vscode.Range(
document.positionAt(0),
document.positionAt(currentCode.length)
)
await executeSmartApply({
configuration: {
id,
document,
range,
replacement,
instruction: ps``,
model: 'skip',
traceparent,
},
source: 'chat',
})
return
}
}

const visibleEditor = vscode.window.visibleTextEditors.find(
editor => editor.document.uri.toString() === document.uri.toString()
)
Expand Down
4 changes: 3 additions & 1 deletion vscode/webviews/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
id: string,
text: string,
instruction?: PromptString,
fileName?: string
fileName?: string,
regex?: string
): void => {
const spanManager = new SpanManager('cody-webview')
const span = spanManager.startSpan('smartApplySubmit', {
Expand All @@ -156,6 +157,7 @@ export const Chat: React.FunctionComponent<React.PropsWithChildren<ChatboxProps>
code: text.replace(/\n$/, ''),
fileName,
traceparent,
regex,
})
span.end()
},
Expand Down
16 changes: 12 additions & 4 deletions vscode/webviews/chat/ChatMessageContent/ChatMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@ export interface CodeBlockActionsProps {
copyButtonOnSubmit: (text: string, event?: 'Keydown' | 'Button') => void
insertButtonOnSubmit: (text: string, newFile?: boolean) => void
smartApply: {
onSubmit: (id: string, text: string, instruction?: PromptString, fileName?: string) => void
onSubmit: (
id: string,
text: string,
instruction?: PromptString,
fileName?: string,
regex?: string
) => void
onAccept: (id: string) => void
onReject: (id: string) => void
}
Expand Down Expand Up @@ -64,13 +70,13 @@ export const ChatMessageContent: React.FunctionComponent<ChatMessageContentProps

return {
...smartApply,
onSubmit(id, text, instruction, fileName) {
onSubmit(id, text, instruction, fileName, regex) {
// We intercept the `onSubmit` to mark this task as working as early as we can.
// In reality, this will happen once we determine the task selection and _then_ start the task.
// The user does not need to be aware of this, for their purposes this is a single operation.
// We can re-use the `Working` state to simplify our UI logic.
setSmartApplyStates(prev => ({ ...prev, [id]: CodyTaskState.Working }))
return smartApply.onSubmit(id, text, instruction, fileName)
return smartApply.onSubmit(id, text, instruction, fileName, regex)
},
}
}, [smartApply])
Expand Down Expand Up @@ -116,6 +122,7 @@ export const ChatMessageContent: React.FunctionComponent<ChatMessageContentProps
// This allows us to intelligently apply code to the suitable file.
const codeElement = preElement.querySelectorAll('code')?.[0]
const fileName = codeElement?.getAttribute('data-file-path') || undefined
const regex = codeElement?.getAttribute('regex') || undefined // Get the regex attribute
// Check if the code element has either 'language-bash' or 'language-shell' class
const isShellCommand =
codeElement?.classList.contains('language-bash') ||
Expand All @@ -136,7 +143,8 @@ export const ChatMessageContent: React.FunctionComponent<ChatMessageContentProps
config.config.hasEditCapability ? insertButtonOnSubmit : undefined,
smartApplyInterceptor,
smartApplyId,
smartApplyState
smartApplyState,
regex
)
} else {
buttons = createButtons(
Expand Down
16 changes: 10 additions & 6 deletions vscode/webviews/chat/ChatMessageContent/create-buttons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ export function createButtonsExperimentalUI(
insertButtonOnSubmit?: CodeBlockActionsProps['insertButtonOnSubmit'],
smartApply?: CodeBlockActionsProps['smartApply'],
smartApplyId?: string,
smartApplyState?: CodyTaskState
smartApplyState?: CodyTaskState,
regex?: string
): HTMLElement {
const container = document.createElement('div')
container.className = styles.buttonsContainer
Expand Down Expand Up @@ -117,7 +118,8 @@ export function createButtonsExperimentalUI(
smartApply,
smartApplyId,
smartApplyState,
codeBlockName
codeBlockName,
regex
)
smartButton.title = isExecutable ? 'Execute in Terminal' : 'Apply in Editor'
buttons.append(smartButton)
Expand Down Expand Up @@ -270,7 +272,8 @@ function createApplyButton(
smartApply: CodeBlockActionsProps['smartApply'],
smartApplyId: FixupTaskID,
smartApplyState?: CodyTaskState,
fileName?: string
fileName?: string,
regex?: string
): HTMLElement {
const button = document.createElement('button')
button.className = styles.button
Expand Down Expand Up @@ -298,13 +301,14 @@ function createApplyButton(
button.prepend(iconContainer)

button.addEventListener('click', () =>
smartApply.onSubmit(smartApplyId, preText, humanMessage?.text, fileName)
smartApply.onSubmit(smartApplyId, preText, humanMessage?.text, fileName, regex)
)

break
}
default: {
button.innerHTML = 'Apply'
// TODO (bee) temporary for debugging purpose - remove before merging
button.innerHTML = regex ? `Instance (${regex})` : 'Apply'

// Add Sparkle Icon
const iconContainer = document.createElement('div')
Expand All @@ -313,7 +317,7 @@ function createApplyButton(
button.prepend(iconContainer)

button.addEventListener('click', () =>
smartApply.onSubmit(smartApplyId, preText, humanMessage?.text, fileName)
smartApply.onSubmit(smartApplyId, preText, humanMessage?.text, fileName, regex)
)
}
}
Expand Down
22 changes: 14 additions & 8 deletions vscode/webviews/chat/extract-file-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { visit } from 'unist-util-visit'
interface CodeNodeData {
hProperties?: {
'data-file-path'?: string
regex?: string
}
}

Expand All @@ -18,11 +19,11 @@ const LANG_FILE_PATH_REGEX = /^(\w+):(.+)$/
* read later in the code.
*
* Example:
* ```typescript:path/to/file.ts
* ```typescript:path/to/file.ts regex="^(\w+):(.+)$"
* console.log()
* ```
* becomes ->
* <code data-file-path="path/to/file.ts">
* <code data-file-path="path/to/file.ts" regex:^(\w+):(.+)$>
* console.log()
* </code>
*/
Expand All @@ -33,17 +34,22 @@ export const remarkAttachFilePathToCodeBlocks: Plugin<[], Root> = () => {
if (match) {
const [, language, filePath] = match

// Update the node's lang to remove the file path
node.lang = language

const hProperties: CodeNodeData['hProperties'] = {
...(node.data as CodeNodeData)?.hProperties,
// We sanitize spaces in markdown path files using `PromptString` class, now we can convert them back
'data-file-path': filePath.trim().replaceAll('%20', ' '),
}

if (node.meta?.startsWith('regex:')) {
hProperties.regex = node.meta.replace('regex:', '').trim()
}

// Update node data
node.data = {
...node.data,
hProperties: {
...(node.data as CodeNodeData)?.hProperties,
// We sanitize spaces in markdown path files using `PromptString` class, now we can convert them back
'data-file-path': filePath.trim().replaceAll('%20', ' '),
},
hProperties,
}
}
})
Expand Down
2 changes: 2 additions & 0 deletions vscode/webviews/components/MarkdownFromCody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ function markdownPluginProps(): Pick<
...(defaultSchema.attributes?.code || []),
// We use `data-file-path` to attach file path metadata to <code> blocks.
['data-file-path'],
// Attach regex query metadata to <code> blocks.
['regex'],
[
'className',
...Object.keys(SYNTAX_HIGHLIGHTING_LANGUAGES).map(
Expand Down
Loading