This repository was archived by the owner on Aug 1, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 487
Expand file tree
/
Copy pathresponse-transformer.ts
More file actions
201 lines (171 loc) · 7.84 KB
/
Copy pathresponse-transformer.ts
File metadata and controls
201 lines (171 loc) · 7.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import { decode } from 'he'
import type * as vscode from 'vscode'
import type { FixupTask } from '../../non-stop/FixupTask'
import {
PROMPT_TOPICS,
SMART_APPLY_CUSTOM_PROMPT_TOPICS,
SMART_APPLY_MODEL_IDENTIFIERS,
} from '../prompt/constants'
import { matchIndentation } from './match-indentation'
import { matchLanguage } from './match-language'
/**
* Regular expression to match prompt topics.
* Ignores specific numbers
*/
const PROMPT_TOPIC_REGEX = new RegExp(
Object.values(PROMPT_TOPICS)
.map(topic => `<\/?${topic}>`)
.join('|')
.replace(/\d+/g, '\\d+'),
'g'
)
/**
* Regular expressions to identify markdown code blocks, and then strip the start and end delimiters.
* Important for compatibility with different chat models, due to most chat models being trained to output Markdown.
*/
const MARKDOWN_CODE_BLOCK_DELIMITER_START = '```(?:\\w+)?'
const MARKDOWN_CODE_BLOCK_DELIMITER_END = '```'
const MARKDOWN_CODE_BLOCK_START = new RegExp(`^${MARKDOWN_CODE_BLOCK_DELIMITER_START}`)
const MARKDOWN_CODE_BLOCK_END = new RegExp(`${MARKDOWN_CODE_BLOCK_DELIMITER_END}$`)
const MARKDOWN_CODE_BLOCK_REGEX = new RegExp(
`${MARKDOWN_CODE_BLOCK_DELIMITER_START}\\s*([\\s\\S]*?)\\s*${MARKDOWN_CODE_BLOCK_DELIMITER_END}`,
'g'
)
const LEADING_SPACES_AND_NEW_LINES = /^\s*\n/
const LEADING_SPACES = /^[ ]+/
const SMART_APPLY_MODEL_SET = new Set(Object.values(SMART_APPLY_MODEL_IDENTIFIERS))
/**
* Checks if the task is using a smart apply custom model
*/
function taskUsesSmartApplyCustomModel(task: FixupTask): boolean {
return SMART_APPLY_MODEL_SET.has(task.model)
}
/**
* Strips the text of any unnecessary content.
* This includes:
* 1. Prompt topics, e.g. <CODE511>. These are used by the LLM to wrap the output code.
* 2. Markdown code blocks, e.g. ```typescript. Most LLMs are trained to produce Markdown-suitable responses.
*/
function stripText(text: string, task: FixupTask): string {
const strippedText = text
// Strip specific XML tags referenced in the prompt, e.g. <CODE511>
.replaceAll(PROMPT_TOPIC_REGEX, '')
if (task.document.languageId === 'markdown') {
// Return this text as is, we do not want to strip Markdown blocks as they may be valuable
// in Markdown files
return strippedText
}
// Strip Markdown syntax for code blocks, e.g. ```typescript.
return strippedText.replaceAll(MARKDOWN_CODE_BLOCK_REGEX, block =>
block.replace(MARKDOWN_CODE_BLOCK_START, '').replace(MARKDOWN_CODE_BLOCK_END, '')
)
}
function extractSmartApplyCustomModelResponse(text: string, task: FixupTask): string {
if (!taskUsesSmartApplyCustomModel(task)) {
return text
}
const openingTag = `<${SMART_APPLY_CUSTOM_PROMPT_TOPICS.FINAL_CODE}>`
const closingTag = `</${SMART_APPLY_CUSTOM_PROMPT_TOPICS.FINAL_CODE}>`
const startsWithTag = text.trimStart().startsWith(openingTag)
const endsWithTag = text.trimEnd().endsWith(closingTag)
if (!startsWithTag || !endsWithTag) {
return text
}
// Only extract the code between the outermost tags
const startIndex = text.indexOf(openingTag) + openingTag.length
const endIndex = text.lastIndexOf(closingTag)
return text.slice(startIndex, endIndex)
}
/**
* Preserves or removes newlines at the start and end of the text based on the original text.
* If the original text doesn't start/end with a newline, the corresponding newline in the updated text is removed.
*/
function trimLLMNewlines(text: string, original: string): string {
let result = text
// Handle starting newline
if (result.match(/^\r?\n/) && !original.match(/^\r?\n/)) {
result = result.replace(/^\r?\n/, '')
}
if (result.match(/\r?\n$/) && !original.match(/\r?\n$/)) {
result = result.replace(/\r?\n$/, '')
}
return result
}
/**
* Regular expression to detect the *few* entities we actually care about.
* We purposefully limit the named-entity part to the common escaping
* sequences that LLMs emit in source code:
* < > & " '
* Everything else (e.g. , ¤, ©, …) is ignored so that we
* don't accidentally alter code like "¤t_value;".
*/
const POTENTIAL_HTML_ENTITY_REGEX = /&(?:(?:lt|gt|amp|quot|apos)|#\d+|#x[0-9a-fA-F]+);/
/**
* Given the LLM response for a FixupTask, transforms the response
* to make it suitable to insert as code.
* This is handling cases where the LLM response does not __only__ include code.
*/
export function responseTransformer(
text: string,
task: FixupTask,
isMessageInProgress: boolean
): string {
// Skip processing for in-progress messages from smart apply custom models
if (taskUsesSmartApplyCustomModel(task)) {
if (isMessageInProgress || task.mode === 'insert') {
return text
}
const updatedText = extractSmartApplyCustomModelResponse(text, task)
// Preserve newlines only if they were in the original text
return trimLLMNewlines(updatedText, task.original)
}
const strippedText = stripText(text, task)
// Trim leading spaces
// - For `add` insertions, the LLM will attempt to continue the code from the position of the cursor, we handle the `insertionPoint`
// but we should preserve new lines as they may be valuable for spacing
// - For other edits, we already trim the selection to exclude padded whitespace, we only want the start of the incoming text
const trimmedText =
task.intent === 'add'
? strippedText.replace(LEADING_SPACES, '')
: strippedText.replace(LEADING_SPACES_AND_NEW_LINES, '')
// Decode HTML entities only if potential entities are detected.
// this way we avoid decoding code that is not HTML.
// For example, `int* current_ptr = ¤t_value;` should not be decoded.
let decodedText = trimmedText
if (POTENTIAL_HTML_ENTITY_REGEX.test(trimmedText)) {
decodedText = decode(trimmedText)
}
if (!isMessageInProgress) {
if (task.mode === 'insert') {
// For insertions, we want to always ensure we include a new line at the end of the response
// unless we have a selection range that is empty. This is the case when we have an `add`
// intent such as a smart apply insert and there it doesn't make sense to include a new line.
// We do not attempt to match indentation, as we don't have any original text to compare to
return decodedText.endsWith('\n') || task.selectionRange.isEmpty
? decodedText
: decodedText + '\n'
}
// For all other intents, we want to ensure the response matches the original text
// and includes a new line at the end if the original text ends with a new line.
// ex when you ask for an edit task if the response doesn't end with a new line
// text from next line will be appended to the response.
decodedText =
task.original.endsWith('\n') && !decodedText.endsWith('\n')
? decodedText + '\n'
: decodedText
return formatToMatchOriginal(decodedText, task.original, task.fixupFile.uri)
}
return decodedText
}
function formatToMatchOriginal(incoming: string, original: string, uri: vscode.Uri): string {
const formattedToMatchLanguage = matchLanguage(incoming, original, uri)
// LLMs have a tendency to complete the response with a final new line, but we don't want to
// include this unless necessary, as we already trim the users' selection, and any additional whitespace will
// hurt the readability of the diff.
const trimmedReplacement =
original.trimEnd().length === original.length
? formattedToMatchLanguage.trimEnd()
: formattedToMatchLanguage
// Attempt to match the indentation of the replacement with the original text
return matchIndentation(trimmedReplacement, original)
}