Path: blob/main/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts
4780 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 { URI } from '../../../../../base/common/uri.js';6import { IPosition, Position } from '../../../../../editor/common/core/position.js';7import { Range } from '../../../../../editor/common/core/range.js';8import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';9import { IChatAgentData, IChatAgentService } from '../participants/chatAgents.js';10import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js';11import { IChatSlashCommandService } from '../participants/chatSlashCommands.js';12import { IChatVariablesService, IDynamicVariable } from '../attachments/chatVariables.js';13import { ChatAgentLocation, ChatModeKind } from '../constants.js';14import { IToolData, ToolSet } from '../tools/languageModelToolsService.js';15import { IPromptsService } from '../promptSyntax/service/promptsService.js';1617const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent18const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)19const slashReg = /^\/([\p{L}\d_\-\.:]+)(?=(\s|$|\b))/iu; // A / command2021export interface IChatParserContext {22/** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */23selectedAgent?: IChatAgentData;24mode?: ChatModeKind;25/** Parse as this agent, even when it does not appear in the query text */26forcedAgent?: IChatAgentData;27}2829export class ChatRequestParser {30constructor(31@IChatAgentService private readonly agentService: IChatAgentService,32@IChatVariablesService private readonly variableService: IChatVariablesService,33@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,34@IPromptsService private readonly promptsService: IPromptsService,35) { }3637parseChatRequest(sessionResource: URI, message: string, location: ChatAgentLocation = ChatAgentLocation.Chat, context?: IChatParserContext): IParsedChatRequest {38const parts: IParsedChatRequestPart[] = [];39const references = this.variableService.getDynamicVariables(sessionResource); // must access this list before any async calls40const toolsByName = new Map<string, IToolData>();41const toolSetsByName = new Map<string, ToolSet>();42for (const [entry, enabled] of this.variableService.getSelectedToolAndToolSets(sessionResource)) {43if (enabled) {44if (entry instanceof ToolSet) {45toolSetsByName.set(entry.referenceName, entry);46} else {47toolsByName.set(entry.toolReferenceName ?? entry.displayName, entry);48}49}50}5152let lineNumber = 1;53let column = 1;54for (let i = 0; i < message.length; i++) {55const previousChar = message.charAt(i - 1);56const char = message.charAt(i);57let newPart: IParsedChatRequestPart | undefined;58if (previousChar.match(/\s/) || i === 0) {59if (char === chatVariableLeader) {60newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName, toolSetsByName);61} else if (char === chatAgentLeader) {62newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);63} else if (char === chatSubcommandLeader) {64newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);65}6667if (!newPart) {68newPart = this.tryToParseDynamicVariable(message.slice(i), i, new Position(lineNumber, column), references);69}70}7172if (newPart) {73if (i !== 0) {74// Insert a part for all the text we passed over, then insert the new parsed part75const previousPart = parts.at(-1);76const previousPartEnd = previousPart?.range.endExclusive ?? 0;77const previousPartEditorRangeEndLine = previousPart?.editorRange.endLineNumber ?? 1;78const previousPartEditorRangeEndCol = previousPart?.editorRange.endColumn ?? 1;79parts.push(new ChatRequestTextPart(80new OffsetRange(previousPartEnd, i),81new Range(previousPartEditorRangeEndLine, previousPartEditorRangeEndCol, lineNumber, column),82message.slice(previousPartEnd, i)));83}8485parts.push(newPart);86}8788if (char === '\n') {89lineNumber++;90column = 1;91} else {92column++;93}94}9596const lastPart = parts.at(-1);97const lastPartEnd = lastPart?.range.endExclusive ?? 0;98if (lastPartEnd < message.length) {99parts.push(new ChatRequestTextPart(100new OffsetRange(lastPartEnd, message.length),101new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column),102message.slice(lastPartEnd, message.length)));103}104105return {106parts,107text: message,108};109}110111private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: Array<IParsedChatRequestPart>, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | undefined {112const nextAgentMatch = message.match(agentReg);113if (!nextAgentMatch) {114return;115}116117const [full, name] = nextAgentMatch;118const agentRange = new OffsetRange(offset, offset + full.length);119const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);120121let agents = this.agentService.getAgentsByName(name);122if (!agents.length) {123const fqAgent = this.agentService.getAgentByFullyQualifiedId(name);124if (fqAgent) {125agents = [fqAgent];126}127}128129// 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 the130// context and we use that one.131const agent = agents.length > 1 && context?.selectedAgent ?132context.selectedAgent :133agents.find((a) => a.locations.includes(location));134if (!agent) {135return;136}137138if (context?.mode && !agent.modes.includes(context.mode)) {139return;140}141142if (parts.some(p => p instanceof ChatRequestAgentPart)) {143// Only one agent allowed144return;145}146147// The agent must come first148if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart))) {149return;150}151152const previousPart = parts.at(-1);153const previousPartEnd = previousPart?.range.endExclusive ?? 0;154const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);155if (textSincePreviousPart.trim() !== '') {156return;157}158159return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);160}161162private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, toolsByName: ReadonlyMap<string, IToolData>, toolSetsByName: ReadonlyMap<string, ToolSet>): ChatRequestToolPart | ChatRequestToolSetPart | undefined {163const nextVariableMatch = message.match(variableReg);164if (!nextVariableMatch) {165return;166}167168const [full, name] = nextVariableMatch;169const varRange = new OffsetRange(offset, offset + full.length);170const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);171172const tool = toolsByName.get(name);173if (tool) {174return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon);175}176177const toolset = toolSetsByName.get(name);178if (toolset) {179const value = Array.from(toolset.getTools()).map(t => new ChatRequestToolPart(varRange, varEditorRange, t.toolReferenceName ?? t.displayName, t.id, t.displayName, t.icon).toVariableEntry());180return new ChatRequestToolSetPart(varRange, varEditorRange, toolset.id, toolset.referenceName, toolset.icon, value);181}182183return;184}185186private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | ChatRequestSlashPromptPart | undefined {187const nextSlashMatch = remainingMessage.match(slashReg);188if (!nextSlashMatch) {189return;190}191192if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) {193// no other part than agent or non-whitespace text allowed: that also means no other slash command194return;195}196197// only whitespace after the last part198const previousPart = parts.at(-1);199const previousPartEnd = previousPart?.range.endExclusive ?? 0;200const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);201if (textSincePreviousPart.trim() !== '') {202return;203}204205const [full, command] = nextSlashMatch;206const slashRange = new OffsetRange(offset, offset + full.length);207const slashEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);208209const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart)?.agent ??210(context?.forcedAgent ? context.forcedAgent : undefined);211if (usedAgent) {212const subCommand = usedAgent.slashCommands.find(c => c.name === command);213if (subCommand) {214// Valid agent subcommand215return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);216}217} else {218const slashCommands = this.slashCommandService.getCommands(location, context?.mode ?? ChatModeKind.Ask);219const slashCommand = slashCommands.find(c => c.command === command);220if (slashCommand) {221// Valid standalone slash command222return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand);223} else {224// check for with default agent for this location225const defaultAgent = this.agentService.getDefaultAgent(location, context?.mode);226const subCommand = defaultAgent?.slashCommands.find(c => c.name === command);227if (subCommand) {228// Valid default agent subcommand229return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);230}231}232233// if there's no agent, asume it is a prompt slash command234const isPromptCommand = this.promptsService.isValidSlashCommandName(command);235if (isPromptCommand) {236return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, command);237}238}239return;240}241242private tryToParseDynamicVariable(message: string, offset: number, position: IPosition, references: ReadonlyArray<IDynamicVariable>): ChatRequestDynamicVariablePart | undefined {243const refAtThisPosition = references.find(r =>244r.range.startLineNumber === position.lineNumber &&245r.range.startColumn === position.column);246if (refAtThisPosition) {247const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn;248const text = message.substring(0, length);249const range = new OffsetRange(offset, offset + length);250return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data, refAtThisPosition.fullName, refAtThisPosition.icon, refAtThisPosition.isFile, refAtThisPosition.isDirectory);251}252253return;254}255}256257258