mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-18 13:17:05 +01:00
Extract chat session routing heuristics (#329522)
* Extract chat session routing heuristics * Export routing enrichment limit * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
9950fa3b6c
commit
ae1d0bab11
@@ -0,0 +1,159 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';
|
||||
import { IRoutableSession, isHighConfidenceSessionRoute, ISessionRouteResult } from '../../common/sessionRouter.js';
|
||||
|
||||
/** Number of top-ranked candidates whose conversation content should be resolved. */
|
||||
export const ROUTE_ENRICH_MAX_CANDIDATES = 12;
|
||||
const RELATED_SESSION_FOLDER_CONFIDENCE = 0.35;
|
||||
|
||||
/**
|
||||
* Extracts the task from an explicit request to start a new session.
|
||||
* Requests that merely mention creating another resource are deliberately ignored.
|
||||
*/
|
||||
export function parseExplicitNewSessionRequest(utterance: string): string | undefined {
|
||||
const match = /^(?:please\s+)?(?:create|start|open)\s+(?:a\s+)?new\s+(?:chat\s+)?session(?:\s+(?:to|for|and)\s+|\s*[:,-]\s*)(.+)$/i.exec(utterance.trim());
|
||||
const task = match?.[1]?.trim();
|
||||
return task || undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chooses the workspace folder for a newly routed session. Explicit folder
|
||||
* mentions win, followed by a sufficiently related session's folder and then
|
||||
* the caller's default.
|
||||
*/
|
||||
export function resolveNewSessionWorkspaceFolder(
|
||||
utterance: string,
|
||||
folders: readonly IWorkspaceFolder[],
|
||||
results: readonly ISessionRouteResult[],
|
||||
candidates: readonly IRoutableSession[],
|
||||
defaultFolder: URI | undefined,
|
||||
): URI | undefined {
|
||||
return folderMentionedInUtterance(utterance, folders)
|
||||
?? folderFromRelatedSession(results, candidates, folders)
|
||||
?? defaultFolder
|
||||
?? folders[0]?.uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bounds transcript enrichment while preserving the model's preliminary order.
|
||||
* Any remaining slots favor active and recently updated sessions.
|
||||
*/
|
||||
export function selectRouterShortlist(
|
||||
candidates: readonly IRoutableSession[],
|
||||
preliminaryResults: readonly ISessionRouteResult[],
|
||||
limit: number = ROUTE_ENRICH_MAX_CANDIDATES,
|
||||
): IRoutableSession[] {
|
||||
if (candidates.length <= limit) {
|
||||
return [...candidates];
|
||||
}
|
||||
|
||||
const candidatesById = new Map(candidates.map(candidate => [candidate.sessionId, candidate]));
|
||||
const selectedIds = new Set<string>();
|
||||
const shortlist: IRoutableSession[] = [];
|
||||
for (const result of preliminaryResults) {
|
||||
const candidate = candidatesById.get(result.sessionId);
|
||||
if (candidate && !selectedIds.has(candidate.sessionId)) {
|
||||
selectedIds.add(candidate.sessionId);
|
||||
shortlist.push(candidate);
|
||||
if (shortlist.length === limit) {
|
||||
return shortlist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fallback = candidates
|
||||
.filter(candidate => !selectedIds.has(candidate.sessionId))
|
||||
.sort((a, b) =>
|
||||
sessionStatusPriority(b.status) - sessionStatusPriority(a.status)
|
||||
|| (b.lastActivity ?? 0) - (a.lastActivity ?? 0)
|
||||
|| a.label.localeCompare(b.label)
|
||||
|| a.sessionId.localeCompare(b.sessionId));
|
||||
shortlist.push(...fallback.slice(0, limit - shortlist.length));
|
||||
return shortlist;
|
||||
}
|
||||
|
||||
/** Selects the top result only when it clears the shared confidence threshold. */
|
||||
export function selectBestSessionRoute(results: readonly ISessionRouteResult[]): ISessionRouteResult | undefined {
|
||||
const top = results[0];
|
||||
return top && isHighConfidenceSessionRoute(top) ? top : undefined;
|
||||
}
|
||||
|
||||
function sessionStatusPriority(status: string | undefined): number {
|
||||
return status === 'working' ? 2 : status === 'idle' ? 1 : 0;
|
||||
}
|
||||
|
||||
function folderMentionedInUtterance(utterance: string, folders: readonly IWorkspaceFolder[]): URI | undefined {
|
||||
const normalizedUtterance = utterance.toLocaleLowerCase();
|
||||
let best: { folder: IWorkspaceFolder; length: number } | undefined;
|
||||
for (const folder of folders) {
|
||||
const names = new Set([folder.name, folder.uri.path.split('/').filter(Boolean).at(-1)]);
|
||||
for (const name of names) {
|
||||
if (!name || name.length < 3) {
|
||||
continue;
|
||||
}
|
||||
const normalizedName = name.toLocaleLowerCase();
|
||||
let start = normalizedUtterance.indexOf(normalizedName);
|
||||
while (start >= 0) {
|
||||
if (isWordBoundary(normalizedUtterance[start - 1])
|
||||
&& isWordBoundary(normalizedUtterance[start + normalizedName.length])) {
|
||||
if (!best || normalizedName.length > best.length) {
|
||||
best = { folder, length: normalizedName.length };
|
||||
}
|
||||
break;
|
||||
}
|
||||
start = normalizedUtterance.indexOf(normalizedName, start + normalizedName.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return best?.folder.uri;
|
||||
}
|
||||
|
||||
function folderFromRelatedSession(
|
||||
results: readonly ISessionRouteResult[],
|
||||
candidates: readonly IRoutableSession[],
|
||||
folders: readonly IWorkspaceFolder[],
|
||||
): URI | undefined {
|
||||
const candidateById = new Map(candidates.map(candidate => [candidate.sessionId, candidate]));
|
||||
for (const result of results) {
|
||||
if (result.confidence < RELATED_SESSION_FOLDER_CONFIDENCE) {
|
||||
continue;
|
||||
}
|
||||
const candidate = candidateById.get(result.sessionId);
|
||||
const folder = candidate && folderForSessionMetadata(candidate, folders);
|
||||
if (folder) {
|
||||
return folder.uri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function folderForSessionMetadata(candidate: IRoutableSession, folders: readonly IWorkspaceFolder[]): IWorkspaceFolder | undefined {
|
||||
for (const path of [candidate.cwd, candidate.repo]) {
|
||||
if (!path) {
|
||||
continue;
|
||||
}
|
||||
const normalizedPath = path.replaceAll('\\', '/').replace(/\/+$/, '').replace(/^([a-zA-Z]:\/)/, '/$1').toLocaleLowerCase();
|
||||
const match = folders
|
||||
.filter(folder => {
|
||||
const folderPath = folder.uri.path.replace(/\/+$/, '').toLocaleLowerCase();
|
||||
return normalizedPath === folderPath
|
||||
|| normalizedPath.startsWith(`${folderPath}/`)
|
||||
|| normalizedPath.endsWith(`/${folder.name.toLocaleLowerCase()}`)
|
||||
|| normalizedPath.endsWith(`/${folder.name.toLocaleLowerCase()}.git`);
|
||||
})
|
||||
.sort((a, b) => b.uri.path.length - a.uri.path.length)[0];
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isWordBoundary(value: string | undefined): boolean {
|
||||
return value === undefined || !/[\p{L}\p{N}_-]/u.test(value);
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
|
||||
import { IWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js';
|
||||
import { parseExplicitNewSessionRequest, resolveNewSessionWorkspaceFolder, selectBestSessionRoute, selectRouterShortlist } from '../../../browser/sessionRouter/chatSessionRoutingHelpers.js';
|
||||
|
||||
suite('Chat session routing helpers', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
const vscode = folder('vscode', '/work/vscode', 0);
|
||||
const docs = folder('vscode-docs', '/work/vscode-docs', 1);
|
||||
|
||||
test('chooses an explicitly mentioned workspace folder', () => {
|
||||
const result = resolveNewSessionWorkspaceFolder(
|
||||
'update the vscode-docs API reference',
|
||||
[vscode, docs],
|
||||
[],
|
||||
[],
|
||||
vscode.uri,
|
||||
);
|
||||
|
||||
assert.strictEqual(result?.toString(), docs.uri.toString());
|
||||
});
|
||||
|
||||
test('uses a related session working directory when starting a new session', () => {
|
||||
const result = resolveNewSessionWorkspaceFolder(
|
||||
'continue the authentication cleanup',
|
||||
[vscode, docs],
|
||||
[{ sessionId: 'related', confidence: 0.5 }],
|
||||
[{ sessionId: 'related', label: 'Authentication cleanup', cwd: '/work/vscode-docs/src' }],
|
||||
vscode.uri,
|
||||
);
|
||||
|
||||
assert.strictEqual(result?.toString(), docs.uri.toString());
|
||||
});
|
||||
|
||||
test('explicit folder mention overrides a related session in another folder', () => {
|
||||
const result = resolveNewSessionWorkspaceFolder(
|
||||
'update the vscode-docs API reference',
|
||||
[vscode, docs],
|
||||
[{ sessionId: 'related', confidence: 0.9 }],
|
||||
[{ sessionId: 'related', label: 'Related work', cwd: '/work/vscode/src' }],
|
||||
vscode.uri,
|
||||
);
|
||||
|
||||
assert.strictEqual(result?.toString(), docs.uri.toString());
|
||||
});
|
||||
|
||||
test('bounds transcript enrichment after every candidate receives model scoring', () => {
|
||||
const candidates = Array.from({ length: 13 }, (_, index) => ({
|
||||
sessionId: `s${index}`,
|
||||
label: `Session ${index}`,
|
||||
status: index === 12 ? 'working' : 'idle',
|
||||
lastActivity: index,
|
||||
}));
|
||||
const shortlist = selectRouterShortlist(candidates, [
|
||||
{ sessionId: 's0', confidence: 0.9 },
|
||||
{ sessionId: 's3', confidence: 0.8 },
|
||||
]);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
length: shortlist.length,
|
||||
first: shortlist[0].sessionId,
|
||||
second: shortlist[1].sessionId,
|
||||
third: shortlist[2].sessionId,
|
||||
excluded: candidates.filter(candidate => !shortlist.includes(candidate)).map(candidate => candidate.sessionId),
|
||||
}, {
|
||||
length: 12,
|
||||
first: 's0',
|
||||
second: 's3',
|
||||
third: 's12',
|
||||
excluded: ['s1'],
|
||||
});
|
||||
});
|
||||
|
||||
test('selects only a high-confidence route', () => {
|
||||
assert.deepStrictEqual(selectBestSessionRoute([
|
||||
{ sessionId: 'best', confidence: 0.9 },
|
||||
{ sessionId: 'previous', confidence: 0.86 },
|
||||
]), { sessionId: 'best', confidence: 0.9 });
|
||||
assert.strictEqual(selectBestSessionRoute([{ sessionId: 'weak', confidence: 0.8 }]), undefined);
|
||||
});
|
||||
|
||||
test('keeps the default folder for a weak related-session match', () => {
|
||||
const result = resolveNewSessionWorkspaceFolder(
|
||||
'start something new',
|
||||
[vscode, docs],
|
||||
[{ sessionId: 'weak', confidence: 0.1 }],
|
||||
[{ sessionId: 'weak', label: 'Unrelated docs work', cwd: '/work/vscode-docs' }],
|
||||
vscode.uri,
|
||||
);
|
||||
|
||||
assert.strictEqual(result?.toString(), vscode.uri.toString());
|
||||
});
|
||||
|
||||
test('extracts only explicit new-session tasks', () => {
|
||||
assert.strictEqual(parseExplicitNewSessionRequest('Create a new session to update the chocolate file'), 'update the chocolate file');
|
||||
assert.strictEqual(parseExplicitNewSessionRequest('Please start a new chat session for fixing tests'), 'fixing tests');
|
||||
assert.strictEqual(parseExplicitNewSessionRequest('Create a new session'), undefined);
|
||||
assert.strictEqual(parseExplicitNewSessionRequest('Create a file in the current session'), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
function folder(name: string, path: string, index: number): IWorkspaceFolder {
|
||||
const uri = URI.file(path);
|
||||
return { uri, name, index, toResource: relativePath => URI.joinPath(uri, relativePath) };
|
||||
}
|
||||
Reference in New Issue
Block a user