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

feat(chat): Include history from messages to docs chatbot VSCODE-632 #871

Merged
merged 65 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
a54d67d
Filter long / invalid prompts
gagik Oct 29, 2024
6ff0d8d
Try helper include
gagik Oct 30, 2024
c9aa589
Use content value
gagik Oct 30, 2024
698051f
Use a cleaner test
gagik Oct 30, 2024
bf05722
delete old helper
gagik Oct 30, 2024
2445dc5
Add explainer
gagik Oct 30, 2024
ff0c1cc
WIP
gagik Oct 31, 2024
f9dd536
Move around dependencies
gagik Nov 1, 2024
09c7414
Remove grep
gagik Nov 1, 2024
aada8e1
Use firstCall
gagik Nov 1, 2024
eb2c7dc
Add test filtering
gagik Nov 1, 2024
bcd260b
Update CONTRIBUTING.md
gagik Nov 1, 2024
a9beef1
Escape the environment variable
gagik Nov 1, 2024
2e4bd70
Merge branch 'gagik/add-test-filtering' of github.com:mongodb-js/vsco…
gagik Nov 1, 2024
33c995e
Fix wording
gagik Nov 1, 2024
8582193
Add schema tests
gagik Nov 1, 2024
a8bc30d
align tests and use a stub
gagik Nov 1, 2024
5ddc7fb
Add saving to metadata
gagik Nov 1, 2024
06435e8
Move things
gagik Nov 3, 2024
5a58171
Better org
gagik Nov 3, 2024
fdbabfc
Merge branch 'gagik/add-test-filtering' of github.com:mongodb-js/vsco…
gagik Nov 3, 2024
f23e19f
simplify tests and picking logic
gagik Nov 3, 2024
274fe17
typos
gagik Nov 3, 2024
54885d6
Align with broken test
gagik Nov 4, 2024
cfb9d61
Add error info and tests
gagik Nov 4, 2024
f080016
wrap l10n
gagik Nov 4, 2024
190f4f6
Merge branch 'gagik/one-no-collection-handling' of github.com:mongodb…
gagik Nov 5, 2024
182dc15
wrap in l10n
gagik Nov 5, 2024
a341e45
remove settings change
gagik Nov 5, 2024
bec38d8
Merge branch 'gagik/no-database-or-collection-error' of github.com:mo…
gagik Nov 5, 2024
548f247
Merge branch 'gagik/fix-long-prompts' of github.com:mongodb-js/vscode…
gagik Nov 5, 2024
c1b6534
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/no-dat…
gagik Nov 5, 2024
378b202
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/one-no…
gagik Nov 5, 2024
f468adf
Add tests
gagik Nov 5, 2024
62c07ef
Apply suggestions from code review
gagik Nov 6, 2024
f2b54ea
changes from review
gagik Nov 6, 2024
276ce90
move error types to inside participant
gagik Nov 6, 2024
bf2a0c7
switch to parametrized tests
gagik Nov 6, 2024
02f45dc
Merge branch 'gagik/one-no-collection-handling' into gagik/no-databas…
gagik Nov 6, 2024
a1765ef
remove patched vscode
gagik Nov 6, 2024
ccacb8a
remove vscode
gagik Nov 6, 2024
886aa45
better comments
gagik Nov 6, 2024
52dbdf6
fix potential CI discrepancy
gagik Nov 7, 2024
686c7cd
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/one-no…
gagik Nov 7, 2024
fee668e
combine message text
gagik Nov 7, 2024
e1d12bf
add explicit undefined returns
gagik Nov 7, 2024
c92cdcb
Merge branch 'gagik/one-no-collection-handling' into gagik/no-databas…
gagik Nov 7, 2024
985ed49
Move tests to parameterized
gagik Nov 7, 2024
f012622
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/filter…
gagik Nov 7, 2024
aba650c
delete mistaken duplicate
gagik Nov 7, 2024
650bd79
cleanup tests
gagik Nov 7, 2024
0843c64
fix expected history
gagik Nov 7, 2024
6cb9ad3
revert
gagik Nov 7, 2024
0bbeeb2
Merge branch 'main' into gagik/filter-namespace
gagik Nov 8, 2024
377b6bd
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/filter…
gagik Nov 8, 2024
853eccf
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/no-dat…
gagik Nov 8, 2024
86a33f2
Merge branch 'gagik/no-database-or-collection-error' of github.com:mo…
gagik Nov 8, 2024
4ad4c5f
small cleanup
gagik Nov 8, 2024
0a8b085
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/filter…
gagik Nov 11, 2024
f2fd248
Refactor
gagik Nov 12, 2024
acd5100
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/add-do…
gagik Nov 12, 2024
2d65348
fix tests and ordering
gagik Nov 12, 2024
3999dd1
Remove log
gagik Nov 15, 2024
622b60a
Add limit and refactor
gagik Nov 17, 2024
5741e99
skip for namespaces
gagik Nov 18, 2024
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
21 changes: 19 additions & 2 deletions src/participant/participant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import { DocsChatbotAIService } from './docsChatbotAIService';
import type TelemetryService from '../telemetry/telemetryService';
import formatError from '../utils/formatError';
import type { ModelInput } from './prompts/promptBase';
import { getContent, type ModelInput } from './prompts/promptBase';
import { processStreamWithIdentifiers } from './streamParsing';
import type { PromptIntent } from './prompts/intent';
import type { DataService } from 'mongodb-data-service';
Expand Down Expand Up @@ -1415,10 +1415,12 @@ export default class ParticipantController {
chatId,
token,
stream,
context,
}: {
prompt: string;
chatId: string;
token: vscode.CancellationToken;
context: vscode.ChatContext;
stream: vscode.ChatResponseStream;
}): Promise<{
responseContent: string;
Expand Down Expand Up @@ -1446,8 +1448,22 @@ export default class ParticipantController {
log.info('Docs chatbot created for chatId', chatId);
}

const history = Prompts.docs.getHistoryMessages({
connectionNames: this._getConnectionNames(),
context: context,
});

const previousMessages =
history.length > 0
gagik marked this conversation as resolved.
Show resolved Hide resolved
? `${history
.map((message: vscode.LanguageModelChatMessage) =>
getContent(message)
)
.join('\n\n')}\n\n`
: '';

const response = await this._docsChatbotAIService.addMessage({
message: prompt,
message: `${previousMessages}${prompt}`,
conversationId: docsChatbotConversationId,
signal: abortController.signal,
});
Expand Down Expand Up @@ -1553,6 +1569,7 @@ export default class ParticipantController {
chatId,
token,
stream,
context,
});

if (docsResult.responseContent) {
Expand Down
44 changes: 44 additions & 0 deletions src/participant/prompts/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type * as vscode from 'vscode';
import type { PromptArgsBase } from './promptBase';
import { PromptBase } from './promptBase';

export class DocsPrompt extends PromptBase<PromptArgsBase> {
alenakhineika marked this conversation as resolved.
Show resolved Hide resolved
protected getAssistantPrompt(): string {
throw new Error('Not used with docs command');
}

getHistoryMessages({
connectionNames,
context,
databaseName,
collectionName,
}: {
connectionNames?: string[];
context?: vscode.ChatContext;
databaseName?: string;
collectionName?: string;
}): vscode.LanguageModelChatMessage[] {
if (!context) {
return [];
}
const historySinceLastDocs: (
| vscode.ChatRequestTurn
| vscode.ChatResponseTurn
)[] = [];

for (let i = context.history.length - 1; i >= 0; i--) {
const message = context.history[i];

if (message.command === 'docs') {
break;
}
historySinceLastDocs.push(context.history[i]);
}
return this.getFilteredHistoryMessages({
connectionNames,
history: historySinceLastDocs.reverse(),
databaseName,
collectionName,
});
}
}
2 changes: 2 additions & 0 deletions src/participant/prompts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { IntentPrompt } from './intent';
import { NamespacePrompt } from './namespace';
import { QueryPrompt } from './query';
import { SchemaPrompt } from './schema';
import { DocsPrompt } from './docs';
import { ExportToPlaygroundPrompt } from './exportToPlayground';
import { isContentEmpty } from './promptBase';

Expand All @@ -16,6 +17,7 @@ export class Prompts {
public static namespace = new NamespacePrompt();
public static query = new QueryPrompt();
public static schema = new SchemaPrompt();
public static docs = new DocsPrompt();
public static exportToPlayground = new ExportToPlaygroundPrompt();

public static isPromptEmpty(request: vscode.ChatRequest): boolean {
Expand Down
216 changes: 135 additions & 81 deletions src/participant/prompts/promptBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@ export function getContentLength(
return 0;
}

export function getContent(message: vscode.LanguageModelChatMessage): string {
const content = message.content as any;
if (typeof content === 'string') {
return content;
}

if (Array.isArray(content)) {
return content.reduce((agg: string, element) => {
const value = element?.value ?? element?.content?.value;
if (typeof value === 'string') {
return agg + value;
}

return agg;
}, '');
}

return '';
}

export function isContentEmpty(
message: vscode.LanguageModelChatMessage
): boolean {
Expand Down Expand Up @@ -88,7 +108,10 @@ export abstract class PromptBase<TArgs extends PromptArgsBase> {
}

async buildMessages(args: TArgs): Promise<ModelInput> {
let historyMessages = this.getHistoryMessages(args);
let historyMessages = this.getFilteredHistoryMessages({
history: args.context?.history,
...args,
});
// If the current user's prompt is a connection name, and the last
// message was to connect. We want to use the last
// message they sent before the connection name as their prompt.
Expand Down Expand Up @@ -158,24 +181,116 @@ export abstract class PromptBase<TArgs extends PromptArgsBase> {
};
}

private _handleChatResponseTurn({
messages,
historyItem,
namespaceIsKnown,
}: {
historyItem: vscode.ChatResponseTurn;
messages: vscode.LanguageModelChatMessage[];
namespaceIsKnown: boolean;
}): void {
if (
historyItem.result.errorDetails?.message ===
ParticipantErrorTypes.FILTERED
) {
// If the response led to a filtered error, we do not want the
// error-causing message to be sent again so we remove it.
messages.pop();
return;
}

let message = '';

// Skip a response to an empty user prompt message or connect message.
const responseTypesToSkip: ParticipantResponseType[] = [
'emptyRequest',
'askToConnect',
];

const responseType = (historyItem.result as ChatResult)?.metadata?.intent;
if (responseTypesToSkip.includes(responseType)) {
return;
}

// If the namespace is already known, skip including prompts asking for it.
if (responseType === 'askForNamespace' && namespaceIsKnown) {
return;
}

for (const fragment of historyItem.response) {
if (fragment instanceof vscode.ChatResponseMarkdownPart) {
message += fragment.value.value;

if (
(historyItem.result as ChatResult)?.metadata?.intent ===
'askForNamespace'
) {
// When the message is the assistant asking for part of a namespace,
// we only want to include the question asked, not the user's
// database and collection names in the history item.
break;
}
}
}

// eslint-disable-next-line new-cap
messages.push(vscode.LanguageModelChatMessage.Assistant(message));
gagik marked this conversation as resolved.
Show resolved Hide resolved
}

private _handleChatRequestTurn({
messages,
historyItem,
previousItem,
connectionNames,
namespaceIsKnown,
}: {
historyItem: vscode.ChatRequestTurn;
previousItem: vscode.ChatRequestTurn | vscode.ChatResponseTurn | undefined;
messages: vscode.LanguageModelChatMessage[];
connectionNames: string[] | undefined;
namespaceIsKnown: boolean;
}): void {
if (
historyItem.prompt?.trim().length === 0 ||
connectionNames?.includes(historyItem.prompt)
) {
// When the message is empty or a connection name then we skip it.
// It's probably going to be the response to the connect step.
return;
}

if (previousItem instanceof vscode.ChatResponseTurn) {
const responseIntent = (previousItem.result as ChatResult).metadata
?.intent;

if (responseIntent === 'askForNamespace' && namespaceIsKnown) {
// If the namespace is already known, skip responses to prompts asking for it.
return;
}
}

// eslint-disable-next-line new-cap
messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt));
}

// When passing the history to the model we only want contextual messages
// to be passed. This function parses through the history and returns
// the messages that are valuable to keep.
// eslint-disable-next-line complexity
protected getHistoryMessages({
protected getFilteredHistoryMessages({
connectionNames,
context,
history,
databaseName,
collectionName,
}: {
connectionNames?: string[]; // Used to scrape the connecting messages from the history.
context?: vscode.ChatContext;
history?: vscode.ChatContext['history'];
databaseName?: string;
collectionName?: string;
}): vscode.LanguageModelChatMessage[] {
const messages: vscode.LanguageModelChatMessage[] = [];

if (!context) {
if (!history) {
return [];
}

Expand All @@ -186,82 +301,21 @@ export abstract class PromptBase<TArgs extends PromptArgsBase> {

const namespaceIsKnown =
databaseName !== undefined && collectionName !== undefined;
for (const historyItem of context.history) {
for (const historyItem of history) {
if (historyItem instanceof vscode.ChatRequestTurn) {
if (
historyItem.prompt?.trim().length === 0 ||
connectionNames?.includes(historyItem.prompt)
) {
// When the message is empty or a connection name then we skip it.
// It's probably going to be the response to the connect step.
previousItem = historyItem;
continue;
}

if (previousItem instanceof vscode.ChatResponseTurn) {
const responseIntent = (previousItem.result as ChatResult).metadata
?.intent;

// If the namespace is already known, skip responses to prompts asking for it.
if (responseIntent === 'askForNamespace' && namespaceIsKnown) {
previousItem = historyItem;
continue;
}
}

// eslint-disable-next-line new-cap
messages.push(vscode.LanguageModelChatMessage.User(historyItem.prompt));
}

if (historyItem instanceof vscode.ChatResponseTurn) {
if (
historyItem.result.errorDetails?.message ===
ParticipantErrorTypes.FILTERED
) {
// If the response led to a filtered error, we do not want the
// error-causing message to be sent again so we remove it.
messages.pop();
continue;
}

let message = '';

// Skip a response to an empty user prompt message or connect message.
const responseTypesToSkip: ParticipantResponseType[] = [
'emptyRequest',
'askToConnect',
];

const responseType = (historyItem.result as ChatResult)?.metadata
?.intent;
if (responseTypesToSkip.includes(responseType)) {
previousItem = historyItem;
continue;
}

// If the namespace is already known, skip including prompts asking for it.
if (responseType === 'askForNamespace' && namespaceIsKnown) {
previousItem = historyItem;
continue;
}

for (const fragment of historyItem.response) {
if (fragment instanceof vscode.ChatResponseMarkdownPart) {
message += fragment.value.value;

if (
(historyItem.result as ChatResult)?.metadata?.intent ===
'askForNamespace'
) {
// When the message is the assistant asking for part of a namespace,
// we only want to include the question asked, not the user's
// database and collection names in the history item.
break;
}
}
}
// eslint-disable-next-line new-cap
messages.push(vscode.LanguageModelChatMessage.Assistant(message));
this._handleChatRequestTurn({
messages,
historyItem,
previousItem,
connectionNames,
namespaceIsKnown,
});
} else if (historyItem instanceof vscode.ChatResponseTurn) {
this._handleChatResponseTurn({
messages,
historyItem,
namespaceIsKnown,
});
}
previousItem = historyItem;
}
Expand Down
Loading
Loading