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): automatically pick the database & collection if there exists only one VSCODE-610 #863

Merged
merged 35 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
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
f080016
wrap l10n
gagik Nov 4, 2024
378b202
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/one-no…
gagik Nov 5, 2024
62c07ef
Apply suggestions from code review
gagik Nov 6, 2024
f2b54ea
changes from review
gagik Nov 6, 2024
bf2a0c7
switch to parametrized tests
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
ae7af00
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/one-no…
gagik Nov 7, 2024
9292c5f
Use command from test
gagik Nov 8, 2024
1f5c9f4
Merge branch 'main' of github.com:mongodb-js/vscode into gagik/one-no…
gagik Nov 8, 2024
7391721
resolve correctly
gagik Nov 8, 2024
affdf74
move participant error types
gagik Nov 8, 2024
4dea018
remove redundant test code
gagik Nov 8, 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
248 changes: 183 additions & 65 deletions src/participant/participant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import formatError from '../utils/formatError';
import type { ModelInput } from './prompts/promptBase';
import { processStreamWithIdentifiers } from './streamParsing';
import type { PromptIntent } from './prompts/intent';
import type { DataService } from 'mongodb-data-service';

const log = createLogger('participant');

Expand Down Expand Up @@ -671,64 +672,49 @@ export default class ParticipantController {
}
}

async renderCollectionsTree({
renderCollectionsTree({
collections,
command,
context,
databaseName,
stream,
}: {
collections: Awaited<ReturnType<DataService['listCollections']>>;
command: ParticipantCommand;
databaseName: string;
context: vscode.ChatContext;
stream: vscode.ChatResponseStream;
}): Promise<void> {
const dataService = this._connectionController.getActiveDataService();
if (!dataService) {
return;
}

stream.push(
new vscode.ChatResponseProgressPart('Fetching collection names...')
}): void {
collections.slice(0, MAX_MARKDOWN_LIST_LENGTH).forEach((coll) =>
stream.markdown(
createMarkdownLink({
commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT,
data: {
command,
chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(
context.history
),
databaseName,
collectionName: coll.name,
},
name: coll.name,
})
)
);

try {
const collections = await dataService.listCollections(databaseName);
collections.slice(0, MAX_MARKDOWN_LIST_LENGTH).forEach((coll) =>
stream.markdown(
createMarkdownLink({
commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT,
data: {
command,
chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(
context.history
),
databaseName,
collectionName: coll.name,
},
name: coll.name,
})
)
if (collections.length > MAX_MARKDOWN_LIST_LENGTH) {
stream.markdown(
createMarkdownLink({
commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT,
data: {
command,
chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(
context.history
),
databaseName,
},
name: 'Show more',
})
);
if (collections.length > MAX_MARKDOWN_LIST_LENGTH) {
stream.markdown(
createMarkdownLink({
commandId: EXTENSION_COMMANDS.SELECT_COLLECTION_WITH_PARTICIPANT,
data: {
command,
chatId: ChatMetadataStore.getChatIdFromHistoryOrNewChatId(
context.history
),
databaseName,
},
name: 'Show more',
})
);
}
} catch (error) {
log.error('Unable to fetch collections:', error);

// Users can always do this manually when asked to provide a collection name.
return;
gagik marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -811,7 +797,67 @@ export default class ParticipantController {
};
}

async _askForNamespace({
async _getCollections({
stream,
databaseName,
}: {
stream: vscode.ChatResponseStream;
databaseName: string;
}): Promise<ReturnType<DataService['listCollections']> | undefined> {
stream.push(
new vscode.ChatResponseProgressPart('Fetching collection names...')
);

const dataService = this._connectionController.getActiveDataService();

if (!dataService) {
return;
}
gagik marked this conversation as resolved.
Show resolved Hide resolved
try {
return await dataService.listCollections(databaseName);
} catch (error) {
log.error('Unable to fetch collections:', error);
return;
gagik marked this conversation as resolved.
Show resolved Hide resolved
}
}

/** Gets the collection name if there is only one collection.
* Otherwise returns undefined and asks the user to select the collection. */
async _getOrAskForCollectionName({
context,
databaseName,
stream,
command,
}: {
command: ParticipantCommand;
context: vscode.ChatContext;
databaseName: string;
stream: vscode.ChatResponseStream;
}): Promise<string | undefined> {
const collections = await this._getCollections({ stream, databaseName });

if (collections !== undefined) {
if (collections.length === 1) {
return collections[0].name;
}

stream.markdown(
`Which collection would you like to use within ${databaseName}?\n\n`
);

this.renderCollectionsTree({
collections,
command,
databaseName,
context,
stream,
});
}

return;
}

async _askForDatabaseName({
command,
context,
databaseName,
Expand All @@ -838,16 +884,6 @@ export default class ParticipantController {
context,
stream,
});
} else if (!collectionName) {
stream.markdown(
`Which collection would you like to use within ${databaseName}?\n\n`
);
await this.renderCollectionsTree({
command,
databaseName,
context,
stream,
});
}

return namespaceRequestChatResult({
Expand Down Expand Up @@ -1011,12 +1047,33 @@ export default class ParticipantController {
// we re-ask the question.
const databaseName = lastMessage.metadata.databaseName;
if (databaseName) {
const collections = await this._getCollections({
stream,
databaseName,
});

if (!collections) {
return namespaceRequestChatResult({
databaseName,
collectionName: undefined,
history: context.history,
});
}

// If there is only 1 collection in the database, we can pick it automatically
if (collections.length === 1) {
stream.markdown(Prompts.generic.getEmptyRequestResponse());
return emptyRequestChatResult(context.history);
}

stream.markdown(
vscode.l10n.t(
'Please select a collection by either clicking on an item in the list or typing the name manually in the chat.'
)
);
await this.renderCollectionsTree({

this.renderCollectionsTree({
collections,
command,
databaseName,
context,
Expand All @@ -1043,6 +1100,7 @@ export default class ParticipantController {
}

// @MongoDB /schema
// eslint-disable-next-line complexity
async handleSchemaRequest(
request: vscode.ChatRequest,
context: vscode.ChatContext,
Expand All @@ -1068,14 +1126,16 @@ export default class ParticipantController {
});
}

const { databaseName, collectionName } = await this._getNamespaceFromChat({
const namespace = await this._getNamespaceFromChat({
request,
context,
token,
});
const { databaseName } = namespace;
let { collectionName } = namespace;

if (!databaseName || !collectionName) {
return await this._askForNamespace({
if (!databaseName) {
return await this._askForDatabaseName({
command: '/schema',
context,
databaseName,
Expand All @@ -1084,6 +1144,34 @@ export default class ParticipantController {
});
}

if (!collectionName) {
collectionName = await this._getOrAskForCollectionName({
command: '/schema',
context,
databaseName,
stream,
});

// If the collection name could not get automatically selected,
// then the user has been prompted for it.
if (!collectionName) {
return namespaceRequestChatResult({
databaseName,
collectionName: undefined,
history: context.history,
});
}

// Save the collection name in the metadata.
const chatId = ChatMetadataStore.getChatIdFromHistoryOrNewChatId(
context.history
);
this._chatMetadataStore.setChatMetadata(chatId, {
gagik marked this conversation as resolved.
Show resolved Hide resolved
...this._chatMetadataStore.getChatMetadata(chatId),
collectionName,
});
}

if (token.isCancellationRequested) {
return this._handleCancelledRequest({
context,
Expand Down Expand Up @@ -1194,20 +1282,50 @@ export default class ParticipantController {
// First we ask the model to parse for the database and collection name.
// If they exist, we can then use them in our final completion.
// When they don't exist we ask the user for them.
const { databaseName, collectionName } = await this._getNamespaceFromChat({
const namespace = await this._getNamespaceFromChat({
request,
context,
token,
});
if (!databaseName || !collectionName) {
return await this._askForNamespace({
const { databaseName } = namespace;
let { collectionName } = namespace;

if (!databaseName) {
return await this._askForDatabaseName({
command: '/query',
context,
databaseName,
collectionName,
stream,
});
}
if (!collectionName) {
collectionName = await this._getOrAskForCollectionName({
command: '/query',
context,
databaseName,
stream,
});

// If the collection name could not get automatically selected,
// then the user has been prompted for it.
if (!collectionName) {
return namespaceRequestChatResult({
databaseName,
collectionName: undefined,
history: context.history,
});
}

// Save the collection name in the metadata.
const chatId = ChatMetadataStore.getChatIdFromHistoryOrNewChatId(
context.history
);
this._chatMetadataStore.setChatMetadata(chatId, {
...this._chatMetadataStore.getChatMetadata(chatId),
collectionName,
});
}

if (token.isCancellationRequested) {
return this._handleCancelledRequest({
Expand Down
Loading
Loading