Skip to content

Commit 7eef0e7

Browse files
authored
fix(chat): update response handling to stream and inline code block parsing VSCODE-620 (#835)
1 parent 118aee0 commit 7eef0e7

File tree

10 files changed

+508
-112
lines changed

10 files changed

+508
-112
lines changed

src/participant/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ export type ParticipantResponseType =
1414
| 'askToConnect'
1515
| 'askForNamespace';
1616

17+
export const codeBlockIdentifier = {
18+
start: '```javascript',
19+
end: '```',
20+
};
21+
1722
interface Metadata {
1823
intent: Exclude<ParticipantResponseType, 'askForNamespace' | 'docs'>;
1924
chatId: string;

src/participant/participant.ts

Lines changed: 138 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
docsRequestChatResult,
2222
schemaRequestChatResult,
2323
createCancelledRequestChatResult,
24+
codeBlockIdentifier,
2425
} from './constants';
2526
import { SchemaFormatter } from './schema';
2627
import { getSimplifiedSampleDocuments } from './sampleDocuments';
@@ -38,7 +39,8 @@ import {
3839
} from '../telemetry/telemetryService';
3940
import { DocsChatbotAIService } from './docsChatbotAIService';
4041
import type TelemetryService from '../telemetry/telemetryService';
41-
import { IntentPrompt, type PromptIntent } from './prompts/intent';
42+
import { processStreamWithIdentifiers } from './streamParsing';
43+
import type { PromptIntent } from './prompts/intent';
4244

4345
const log = createLogger('participant');
4446

@@ -59,16 +61,6 @@ export type ParticipantCommand = '/query' | '/schema' | '/docs';
5961

6062
const MAX_MARKDOWN_LIST_LENGTH = 10;
6163

62-
export function getRunnableContentFromString(text: string): string {
63-
const matchedJSresponseContent = text.match(/```javascript((.|\n)*)```/);
64-
65-
const code =
66-
matchedJSresponseContent && matchedJSresponseContent.length > 1
67-
? matchedJSresponseContent[1]
68-
: '';
69-
return code.trim();
70-
}
71-
7264
export default class ParticipantController {
7365
_participant?: vscode.ChatParticipant;
7466
_connectionController: ConnectionController;
@@ -171,48 +163,113 @@ export default class ParticipantController {
171163
});
172164
}
173165

174-
async getChatResponseContent({
166+
async _getChatResponse({
175167
messages,
176168
token,
177169
}: {
178170
messages: vscode.LanguageModelChatMessage[];
179171
token: vscode.CancellationToken;
180-
}): Promise<string> {
172+
}): Promise<vscode.LanguageModelChatResponse> {
181173
const model = await getCopilotModel();
182-
let responseContent = '';
183-
if (model) {
184-
const chatResponse = await model.sendRequest(messages, {}, token);
185-
for await (const fragment of chatResponse.text) {
186-
responseContent += fragment;
187-
}
174+
175+
if (!model) {
176+
throw new Error('Copilot model not found');
188177
}
189178

190-
return responseContent;
179+
return await model.sendRequest(messages, {}, token);
191180
}
192181

193-
_streamRunnableContentActions({
194-
responseContent,
182+
async streamChatResponse({
183+
messages,
195184
stream,
185+
token,
196186
}: {
197-
responseContent: string;
187+
messages: vscode.LanguageModelChatMessage[];
188+
stream: vscode.ChatResponseStream;
189+
token: vscode.CancellationToken;
190+
}): Promise<void> {
191+
const chatResponse = await this._getChatResponse({
192+
messages,
193+
token,
194+
});
195+
for await (const fragment of chatResponse.text) {
196+
stream.markdown(fragment);
197+
}
198+
}
199+
200+
_streamCodeBlockActions({
201+
runnableContent,
202+
stream,
203+
}: {
204+
runnableContent: string;
198205
stream: vscode.ChatResponseStream;
199206
}): void {
200-
const runnableContent = getRunnableContentFromString(responseContent);
201-
if (runnableContent) {
202-
const commandArgs: RunParticipantQueryCommandArgs = {
203-
runnableContent,
204-
};
205-
stream.button({
206-
command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY,
207-
title: vscode.l10n.t('▶️ Run'),
208-
arguments: [commandArgs],
209-
});
210-
stream.button({
211-
command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND,
212-
title: vscode.l10n.t('Open in playground'),
213-
arguments: [commandArgs],
214-
});
207+
runnableContent = runnableContent.trim();
208+
209+
if (!runnableContent) {
210+
return;
215211
}
212+
213+
const commandArgs: RunParticipantQueryCommandArgs = {
214+
runnableContent,
215+
};
216+
stream.button({
217+
command: EXTENSION_COMMANDS.RUN_PARTICIPANT_QUERY,
218+
title: vscode.l10n.t('▶️ Run'),
219+
arguments: [commandArgs],
220+
});
221+
stream.button({
222+
command: EXTENSION_COMMANDS.OPEN_PARTICIPANT_QUERY_IN_PLAYGROUND,
223+
title: vscode.l10n.t('Open in playground'),
224+
arguments: [commandArgs],
225+
});
226+
}
227+
228+
async streamChatResponseContentWithCodeActions({
229+
messages,
230+
stream,
231+
token,
232+
}: {
233+
messages: vscode.LanguageModelChatMessage[];
234+
stream: vscode.ChatResponseStream;
235+
token: vscode.CancellationToken;
236+
}): Promise<void> {
237+
const chatResponse = await this._getChatResponse({
238+
messages,
239+
token,
240+
});
241+
242+
await processStreamWithIdentifiers({
243+
processStreamFragment: (fragment: string) => {
244+
stream.markdown(fragment);
245+
},
246+
onStreamIdentifier: (content: string) => {
247+
this._streamCodeBlockActions({ runnableContent: content, stream });
248+
},
249+
inputIterable: chatResponse.text,
250+
identifier: codeBlockIdentifier,
251+
});
252+
}
253+
254+
// This will stream all of the response content and create a string from it.
255+
// It should only be used when the entire response is needed at one time.
256+
async getChatResponseContent({
257+
messages,
258+
token,
259+
}: {
260+
messages: vscode.LanguageModelChatMessage[];
261+
token: vscode.CancellationToken;
262+
}): Promise<string> {
263+
let responseContent = '';
264+
const chatResponse = await this._getChatResponse({
265+
messages,
266+
token,
267+
});
268+
for await (const fragment of chatResponse.text) {
269+
responseContent += fragment;
270+
}
271+
272+
return responseContent;
216273
}
217274

218275
async _handleRoutedGenericRequest(
@@ -227,14 +284,9 @@ export default class ParticipantController {
227284
connectionNames: this._getConnectionNames(),
228285
});
229286

230-
const responseContent = await this.getChatResponseContent({
287+
await this.streamChatResponseContentWithCodeActions({
231288
messages,
232289
token,
233-
});
234-
stream.markdown(responseContent);
235-
236-
this._streamRunnableContentActions({
237-
responseContent,
238290
stream,
239291
});
240292

@@ -293,7 +345,7 @@ export default class ParticipantController {
293345
token,
294346
});
295347

296-
return IntentPrompt.getIntentFromModelResponse(responseContent);
348+
return Prompts.intent.getIntentFromModelResponse(responseContent);
297349
}
298350

299351
async handleGenericRequest(
@@ -1001,11 +1053,11 @@ export default class ParticipantController {
10011053
connectionNames: this._getConnectionNames(),
10021054
...(sampleDocuments ? { sampleDocuments } : {}),
10031055
});
1004-
const responseContent = await this.getChatResponseContent({
1056+
await this.streamChatResponse({
10051057
messages,
1058+
stream,
10061059
token,
10071060
});
1008-
stream.markdown(responseContent);
10091061

10101062
stream.button({
10111063
command: EXTENSION_COMMANDS.PARTICIPANT_OPEN_RAW_SCHEMA_OUTPUT,
@@ -1104,16 +1156,11 @@ export default class ParticipantController {
11041156
connectionNames: this._getConnectionNames(),
11051157
...(sampleDocuments ? { sampleDocuments } : {}),
11061158
});
1107-
const responseContent = await this.getChatResponseContent({
1108-
messages,
1109-
token,
1110-
});
1111-
1112-
stream.markdown(responseContent);
11131159

1114-
this._streamRunnableContentActions({
1115-
responseContent,
1160+
await this.streamChatResponseContentWithCodeActions({
1161+
messages,
11161162
stream,
1163+
token,
11171164
});
11181165

11191166
return queryRequestChatResult(context.history);
@@ -1181,32 +1228,41 @@ export default class ParticipantController {
11811228
vscode.ChatResponseStream,
11821229
vscode.CancellationToken
11831230
]
1184-
): Promise<{
1185-
responseContent: string;
1186-
responseReferences?: Reference[];
1187-
}> {
1188-
const [request, context, , token] = args;
1231+
): Promise<void> {
1232+
const [request, context, stream, token] = args;
11891233
const messages = await Prompts.generic.buildMessages({
11901234
request,
11911235
context,
11921236
connectionNames: this._getConnectionNames(),
11931237
});
11941238

1195-
const responseContent = await this.getChatResponseContent({
1239+
await this.streamChatResponseContentWithCodeActions({
11961240
messages,
1241+
stream,
11971242
token,
11981243
});
1199-
const responseReferences = [
1200-
{
1244+
1245+
this._streamResponseReference({
1246+
reference: {
12011247
url: MONGODB_DOCS_LINK,
12021248
title: 'View MongoDB documentation',
12031249
},
1204-
];
1250+
stream,
1251+
});
1252+
}
12051253

1206-
return {
1207-
responseContent,
1208-
responseReferences,
1209-
};
1254+
_streamResponseReference({
1255+
reference,
1256+
stream,
1257+
}: {
1258+
reference: Reference;
1259+
stream: vscode.ChatResponseStream;
1260+
}): void {
1261+
const link = new vscode.MarkdownString(
1262+
`- [${reference.title}](${reference.url})\n`
1263+
);
1264+
link.supportHtml = true;
1265+
stream.markdown(link);
12101266
}
12111267

12121268
async handleDocsRequest(
@@ -1235,6 +1291,19 @@ export default class ParticipantController {
12351291
token,
12361292
stream,
12371293
});
1294+
1295+
if (docsResult.responseReferences) {
1296+
for (const reference of docsResult.responseReferences) {
1297+
this._streamResponseReference({
1298+
reference,
1299+
stream,
1300+
});
1301+
}
1302+
}
1303+
1304+
if (docsResult.responseContent) {
1305+
stream.markdown(docsResult.responseContent);
1306+
}
12381307
} catch (error) {
12391308
// If the docs chatbot API is not available, fall back to Copilot’s LLM and include
12401309
// the MongoDB documentation link for users to go to our documentation site directly.
@@ -1255,25 +1324,7 @@ export default class ParticipantController {
12551324
}
12561325
);
12571326

1258-
docsResult = await this._handleDocsRequestWithCopilot(...args);
1259-
}
1260-
1261-
if (docsResult.responseContent) {
1262-
stream.markdown(docsResult.responseContent);
1263-
this._streamRunnableContentActions({
1264-
responseContent: docsResult.responseContent,
1265-
stream,
1266-
});
1267-
}
1268-
1269-
if (docsResult.responseReferences) {
1270-
for (const ref of docsResult.responseReferences) {
1271-
const link = new vscode.MarkdownString(
1272-
`- [${ref.title}](${ref.url})\n`
1273-
);
1274-
link.supportHtml = true;
1275-
stream.markdown(link);
1276-
}
1327+
await this._handleDocsRequestWithCopilot(...args);
12771328
}
12781329

12791330
return docsRequestChatResult({

src/participant/prompts/generic.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import * as vscode from 'vscode';
33
import type { PromptArgsBase } from './promptBase';
44
import { PromptBase } from './promptBase';
55

6+
import { codeBlockIdentifier } from '../constants';
7+
68
export class GenericPrompt extends PromptBase<PromptArgsBase> {
79
protected getAssistantPrompt(): string {
810
return `You are a MongoDB expert.
@@ -12,7 +14,7 @@ Rules:
1214
1. Keep your response concise.
1315
2. You should suggest code that is performant and correct.
1416
3. Respond with markdown.
15-
4. When relevant, provide code in a Markdown code block that begins with \`\`\`javascript and ends with \`\`\`.
17+
4. When relevant, provide code in a Markdown code block that begins with ${codeBlockIdentifier.start} and ends with ${codeBlockIdentifier.end}
1618
5. Use MongoDB shell syntax for code unless the user requests a specific language.
1719
6. If you require additional information to provide a response, ask the user for it.
1820
7. When specifying a database, use the MongoDB syntax use('databaseName').`;

src/participant/prompts/intent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Response:
3434
Docs`;
3535
}
3636

37-
static getIntentFromModelResponse(response: string): PromptIntent {
37+
getIntentFromModelResponse(response: string): PromptIntent {
3838
response = response.trim();
3939
switch (response) {
4040
case 'Query':

0 commit comments

Comments
 (0)