Path: blob/main/src/vs/workbench/contrib/chat/common/chatRequestParser.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';6import { IPosition, Position } from '../../../../editor/common/core/position.js';7import { Range } from '../../../../editor/common/core/range.js';8import { IChatAgentData, IChatAgentService } from './chatAgents.js';9import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js';10import { IChatSlashCommandService } from './chatSlashCommands.js';11import { IChatVariablesService, IDynamicVariable } from './chatVariables.js';12import { ChatAgentLocation, ChatModeKind } from './constants.js';13import { IToolData, ToolSet } from './languageModelToolsService.js';14import { IPromptsService } from './promptSyntax/service/promptsService.js';1516const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent17const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)18const slashReg = /^\/([\p{L}\d_\-\.:]+)(?=(\s|$|\b))/iu; // A / command1920export interface IChatParserContext {21/** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */22selectedAgent?: IChatAgentData;23mode?: ChatModeKind;24}2526export class ChatRequestParser {27constructor(28@IChatAgentService private readonly agentService: IChatAgentService,29@IChatVariablesService private readonly variableService: IChatVariablesService,30@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,31@IPromptsService private readonly promptsService: IPromptsService,32) { }3334parseChatRequest(sessionId: string, message: string, location: ChatAgentLocation = ChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest {35const parts: IParsedChatRequestPart[] = [];36const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls37const toolsByName = new Map<string, IToolData>();38const toolSetsByName = new Map<string, ToolSet>();39for (const [entry, enabled] of this.variableService.getSelectedToolAndToolSets(sessionId)) {40if (enabled) {41if (entry instanceof ToolSet) {42toolSetsByName.set(entry.referenceName, entry);43} else {44toolsByName.set(entry.toolReferenceName ?? entry.displayName, entry);45}46}47}4849let lineNumber = 1;50let column = 1;51for (let i = 0; i < message.length; i++) {52const previousChar = message.charAt(i - 1);53const char = message.charAt(i);54let newPart: IParsedChatRequestPart | undefined;55if (previousChar.match(/\s/) || i === 0) {56if (char === chatVariableLeader) {57newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName, toolSetsByName);58} else if (char === chatAgentLeader) {59newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);60} else if (char === chatSubcommandLeader) {61newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);62}6364if (!newPart) {65newPart = this.tryToParseDynamicVariable(message.slice(i), i, new Position(lineNumber, column), references);66}67}6869if (newPart) {70if (i !== 0) {71// Insert a part for all the text we passed over, then insert the new parsed part72const previousPart = parts.at(-1);73const previousPartEnd = previousPart?.range.endExclusive ?? 0;74const previousPartEditorRangeEndLine = previousPart?.editorRange.endLineNumber ?? 1;75const previousPartEditorRangeEndCol = previousPart?.editorRange.endColumn ?? 1;76parts.push(new ChatRequestTextPart(77new OffsetRange(previousPartEnd, i),78new Range(previousPartEditorRangeEndLine, previousPartEditorRangeEndCol, lineNumber, column),79message.slice(previousPartEnd, i)));80}8182parts.push(newPart);83}8485if (char === '\n') {86lineNumber++;87column = 1;88} else {89column++;90}91}9293const lastPart = parts.at(-1);94const lastPartEnd = lastPart?.range.endExclusive ?? 0;95if (lastPartEnd < message.length) {96parts.push(new ChatRequestTextPart(97new OffsetRange(lastPartEnd, message.length),98new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column),99message.slice(lastPartEnd, message.length)));100}101102return {103parts,104text: message,105};106}107108private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: Array<IParsedChatRequestPart>, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | undefined {109const nextAgentMatch = message.match(agentReg);110if (!nextAgentMatch) {111return;112}113114const [full, name] = nextAgentMatch;115const agentRange = new OffsetRange(offset, offset + full.length);116const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);117118let agents = this.agentService.getAgentsByName(name);119if (!agents.length) {120const fqAgent = this.agentService.getAgentByFullyQualifiedId(name);121if (fqAgent) {122agents = [fqAgent];123}124}125126// If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the127// context and we use that one.128const agent = agents.length > 1 && context?.selectedAgent ?129context.selectedAgent :130agents.find((a) => a.locations.includes(location));131if (!agent) {132return;133}134135if (context?.mode && !agent.modes.includes(context.mode)) {136return;137}138139if (parts.some(p => p instanceof ChatRequestAgentPart)) {140// Only one agent allowed141return;142}143144// The agent must come first145if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart))) {146return;147}148149const previousPart = parts.at(-1);150const previousPartEnd = previousPart?.range.endExclusive ?? 0;151const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);152if (textSincePreviousPart.trim() !== '') {153return;154}155156return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);157}158159private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, toolsByName: ReadonlyMap<string, IToolData>, toolSetsByName: ReadonlyMap<string, ToolSet>): ChatRequestToolPart | ChatRequestToolSetPart | undefined {160const nextVariableMatch = message.match(variableReg);161if (!nextVariableMatch) {162return;163}164165const [full, name] = nextVariableMatch;166const varRange = new OffsetRange(offset, offset + full.length);167const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);168169const tool = toolsByName.get(name);170if (tool) {171return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon);172}173174const toolset = toolSetsByName.get(name);175if (toolset) {176const value = Array.from(toolset.getTools()).map(t => new ChatRequestToolPart(varRange, varEditorRange, t.toolReferenceName ?? t.displayName, t.id, t.displayName, t.icon).toVariableEntry());177return new ChatRequestToolSetPart(varRange, varEditorRange, toolset.id, toolset.referenceName, toolset.icon, value);178}179180return;181}182183private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | ChatRequestSlashPromptPart | undefined {184const nextSlashMatch = remainingMessage.match(slashReg);185if (!nextSlashMatch) {186return;187}188189if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) {190// no other part than agent or non-whitespace text allowed: that also means no other slash command191return;192}193194// only whitespace after the last part195const previousPart = parts.at(-1);196const previousPartEnd = previousPart?.range.endExclusive ?? 0;197const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);198if (textSincePreviousPart.trim() !== '') {199return;200}201202const [full, command] = nextSlashMatch;203const slashRange = new OffsetRange(offset, offset + full.length);204const slashEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);205206const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart);207if (usedAgent) {208const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command);209if (subCommand) {210// Valid agent subcommand211return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);212}213} else {214const slashCommands = this.slashCommandService.getCommands(location, context?.mode ?? ChatModeKind.Ask);215const slashCommand = slashCommands.find(c => c.command === command);216if (slashCommand) {217// Valid standalone slash command218return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand);219} else {220// check for with default agent for this location221const defaultAgent = this.agentService.getDefaultAgent(location, context?.mode);222const subCommand = defaultAgent?.slashCommands.find(c => c.name === command);223if (subCommand) {224// Valid default agent subcommand225return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);226}227}228229// if there's no agent, check if it's a prompt command230const promptCommand = this.promptsService.asPromptSlashCommand(command);231if (promptCommand) {232return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, promptCommand);233}234}235return;236}237238private tryToParseDynamicVariable(message: string, offset: number, position: IPosition, references: ReadonlyArray<IDynamicVariable>): ChatRequestDynamicVariablePart | undefined {239const refAtThisPosition = references.find(r =>240r.range.startLineNumber === position.lineNumber &&241r.range.startColumn === position.column);242if (refAtThisPosition) {243const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn;244const text = message.substring(0, length);245const range = new OffsetRange(offset, offset + length);246return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data, refAtThisPosition.fullName, refAtThisPosition.icon, refAtThisPosition.isFile, refAtThisPosition.isDirectory);247}248249return;250}251}252253254