Skip to content

Commit 8a20e2a

Browse files
authored
feat(chat): Include history from messages to docs chatbot VSCODE-632 (#871)
1 parent 316d188 commit 8a20e2a

File tree

5 files changed

+425
-244
lines changed

5 files changed

+425
-244
lines changed

src/participant/participant.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,12 @@ import {
4040
import { DocsChatbotAIService } from './docsChatbotAIService';
4141
import type TelemetryService from '../telemetry/telemetryService';
4242
import formatError from '../utils/formatError';
43-
import type { ModelInput } from './prompts/promptBase';
43+
import { getContent, type ModelInput } from './prompts/promptBase';
4444
import { processStreamWithIdentifiers } from './streamParsing';
4545
import type { PromptIntent } from './prompts/intent';
4646
import type { DataService } from 'mongodb-data-service';
4747
import { ParticipantErrorTypes } from './participantErrorTypes';
48+
import { PromptHistory } from './prompts/promptHistory';
4849

4950
const log = createLogger('participant');
5051

@@ -1415,10 +1416,12 @@ export default class ParticipantController {
14151416
chatId,
14161417
token,
14171418
stream,
1419+
context,
14181420
}: {
14191421
prompt: string;
14201422
chatId: string;
14211423
token: vscode.CancellationToken;
1424+
context: vscode.ChatContext;
14221425
stream: vscode.ChatResponseStream;
14231426
}): Promise<{
14241427
responseContent: string;
@@ -1446,8 +1449,22 @@ export default class ParticipantController {
14461449
log.info('Docs chatbot created for chatId', chatId);
14471450
}
14481451

1452+
const history = PromptHistory.getFilteredHistoryForDocs({
1453+
connectionNames: this._getConnectionNames(),
1454+
context: context,
1455+
});
1456+
1457+
const previousMessages =
1458+
history.length > 0
1459+
? `${history
1460+
.map((message: vscode.LanguageModelChatMessage) =>
1461+
getContent(message)
1462+
)
1463+
.join('\n\n')}\n\n`
1464+
: '';
1465+
14491466
const response = await this._docsChatbotAIService.addMessage({
1450-
message: prompt,
1467+
message: `${previousMessages}${prompt}`,
14511468
conversationId: docsChatbotConversationId,
14521469
signal: abortController.signal,
14531470
});
@@ -1553,6 +1570,7 @@ export default class ParticipantController {
15531570
chatId,
15541571
token,
15551572
stream,
1573+
context,
15561574
});
15571575

15581576
if (docsResult.responseContent) {

src/participant/prompts/promptBase.ts

Lines changed: 26 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import * as vscode from 'vscode';
2-
import type { ChatResult, ParticipantResponseType } from '../constants';
2+
import type { ChatResult } from '../constants';
33
import type {
44
InternalPromptPurpose,
55
ParticipantPromptProperties,
66
} from '../../telemetry/telemetryService';
7-
import { ParticipantErrorTypes } from '../participantErrorTypes';
7+
import { PromptHistory } from './promptHistory';
88

99
export interface PromptArgsBase {
1010
request: {
@@ -53,6 +53,26 @@ export function getContentLength(
5353
return 0;
5454
}
5555

56+
export function getContent(message: vscode.LanguageModelChatMessage): string {
57+
const content = message.content as any;
58+
if (typeof content === 'string') {
59+
return content;
60+
}
61+
62+
if (Array.isArray(content)) {
63+
return content.reduce((agg: string, element) => {
64+
const value = element?.value ?? element?.content?.value;
65+
if (typeof value === 'string') {
66+
return agg + value;
67+
}
68+
69+
return agg;
70+
}, '');
71+
}
72+
73+
return '';
74+
}
75+
5676
export function isContentEmpty(
5777
message: vscode.LanguageModelChatMessage
5878
): boolean {
@@ -88,7 +108,10 @@ export abstract class PromptBase<TArgs extends PromptArgsBase> {
88108
}
89109

90110
async buildMessages(args: TArgs): Promise<ModelInput> {
91-
let historyMessages = this.getHistoryMessages(args);
111+
let historyMessages = PromptHistory.getFilteredHistory({
112+
history: args.context?.history,
113+
...args,
114+
});
92115
// If the current user's prompt is a connection name, and the last
93116
// message was to connect. We want to use the last
94117
// message they sent before the connection name as their prompt.
@@ -157,115 +180,4 @@ export abstract class PromptBase<TArgs extends PromptArgsBase> {
157180
internal_purpose: this.internalPurposeForTelemetry,
158181
};
159182
}
160-
161-
// When passing the history to the model we only want contextual messages
162-
// to be passed. This function parses through the history and returns
163-
// the messages that are valuable to keep.
164-
// eslint-disable-next-line complexity
165-
protected getHistoryMessages({
166-
connectionNames,
167-
context,
168-
databaseName,
169-
collectionName,
170-
}: {
171-
connectionNames?: string[]; // Used to scrape the connecting messages from the history.
172-
context?: vscode.ChatContext;
173-
databaseName?: string;
174-
collectionName?: string;
175-
}): vscode.LanguageModelChatMessage[] {
176-
const messages: vscode.LanguageModelChatMessage[] = [];
177-
178-
if (!context) {
179-
return [];
180-
}
181-
182-
let previousItem:
183-
| vscode.ChatRequestTurn
184-
| vscode.ChatResponseTurn
185-
| undefined = undefined;
186-
187-
const namespaceIsKnown =
188-
databaseName !== undefined && collectionName !== undefined;
189-
for (const historyItem of context.history) {
190-
if (historyItem instanceof vscode.ChatRequestTurn) {
191-
if (
192-
historyItem.prompt?.trim().length === 0 ||
193-
connectionNames?.includes(historyItem.prompt)
194-
) {
195-
// When the message is empty or a connection name then we skip it.
196-
// It's probably going to be the response to the connect step.
197-
previousItem = historyItem;
198-
continue;
199-
}
200-
201-
if (previousItem instanceof vscode.ChatResponseTurn) {
202-
const responseIntent = (previousItem.result as ChatResult).metadata
203-
?.intent;
204-
205-
// If the namespace is already known, skip responses to prompts asking for it.
206-
if (responseIntent === 'askForNamespace' && namespaceIsKnown) {
207-
previousItem = historyItem;
208-
continue;
209-
}
210-
}
211-
212-
// eslint-disable-next-line new-cap
213-
messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt));
214-
}
215-
216-
if (historyItem instanceof vscode.ChatResponseTurn) {
217-
if (
218-
historyItem.result.errorDetails?.message ===
219-
ParticipantErrorTypes.FILTERED
220-
) {
221-
// If the response led to a filtered error, we do not want the
222-
// error-causing message to be sent again so we remove it.
223-
messages.pop();
224-
continue;
225-
}
226-
227-
let message = '';
228-
229-
// Skip a response to an empty user prompt message or connect message.
230-
const responseTypesToSkip: ParticipantResponseType[] = [
231-
'emptyRequest',
232-
'askToConnect',
233-
];
234-
235-
const responseType = (historyItem.result as ChatResult)?.metadata
236-
?.intent;
237-
if (responseTypesToSkip.includes(responseType)) {
238-
previousItem = historyItem;
239-
continue;
240-
}
241-
242-
// If the namespace is already known, skip including prompts asking for it.
243-
if (responseType === 'askForNamespace' && namespaceIsKnown) {
244-
previousItem = historyItem;
245-
continue;
246-
}
247-
248-
for (const fragment of historyItem.response) {
249-
if (fragment instanceof vscode.ChatResponseMarkdownPart) {
250-
message += fragment.value.value;
251-
252-
if (
253-
(historyItem.result as ChatResult)?.metadata?.intent ===
254-
'askForNamespace'
255-
) {
256-
// When the message is the assistant asking for part of a namespace,
257-
// we only want to include the question asked, not the user's
258-
// database and collection names in the history item.
259-
break;
260-
}
261-
}
262-
}
263-
// eslint-disable-next-line new-cap
264-
messages.push(vscode.LanguageModelChatMessage.Assistant(message));
265-
}
266-
previousItem = historyItem;
267-
}
268-
269-
return messages;
270-
}
271183
}

0 commit comments

Comments
 (0)