Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/requestParser/chatRequestParser.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { URI } from '../../../../../base/common/uri.js';
7
import { IPosition, Position } from '../../../../../editor/common/core/position.js';
8
import { Range } from '../../../../../editor/common/core/range.js';
9
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
10
import { IChatAgentData, IChatAgentService } from '../participants/chatAgents.js';
11
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestSlashPromptPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestToolSetPart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from './chatParserTypes.js';
12
import { IChatSlashCommandService } from '../participants/chatSlashCommands.js';
13
import { IChatVariablesService, IDynamicVariable } from '../attachments/chatVariables.js';
14
import { ChatAgentLocation, ChatModeKind } from '../constants.js';
15
import { IToolData, ToolSet } from '../tools/languageModelToolsService.js';
16
import { IPromptsService } from '../promptSyntax/service/promptsService.js';
17
18
const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent
19
const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2)
20
const slashReg = /^\/([\p{L}\d_\-\.:]+)(?=(\s|$|\b))/iu; // A / command
21
22
export interface IChatParserContext {
23
/** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */
24
selectedAgent?: IChatAgentData;
25
mode?: ChatModeKind;
26
/** Parse as this agent, even when it does not appear in the query text */
27
forcedAgent?: IChatAgentData;
28
}
29
30
export class ChatRequestParser {
31
constructor(
32
@IChatAgentService private readonly agentService: IChatAgentService,
33
@IChatVariablesService private readonly variableService: IChatVariablesService,
34
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService,
35
@IPromptsService private readonly promptsService: IPromptsService,
36
) { }
37
38
parseChatRequest(sessionResource: URI, message: string, location: ChatAgentLocation = ChatAgentLocation.Chat, context?: IChatParserContext): IParsedChatRequest {
39
const parts: IParsedChatRequestPart[] = [];
40
const references = this.variableService.getDynamicVariables(sessionResource); // must access this list before any async calls
41
const toolsByName = new Map<string, IToolData>();
42
const toolSetsByName = new Map<string, ToolSet>();
43
for (const [entry, enabled] of this.variableService.getSelectedToolAndToolSets(sessionResource)) {
44
if (enabled) {
45
if (entry instanceof ToolSet) {
46
toolSetsByName.set(entry.referenceName, entry);
47
} else {
48
toolsByName.set(entry.toolReferenceName ?? entry.displayName, entry);
49
}
50
}
51
}
52
53
let lineNumber = 1;
54
let column = 1;
55
for (let i = 0; i < message.length; i++) {
56
const previousChar = message.charAt(i - 1);
57
const char = message.charAt(i);
58
let newPart: IParsedChatRequestPart | undefined;
59
if (previousChar.match(/\s/) || i === 0) {
60
if (char === chatVariableLeader) {
61
newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts, toolsByName, toolSetsByName);
62
} else if (char === chatAgentLeader) {
63
newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);
64
} else if (char === chatSubcommandLeader) {
65
newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context);
66
}
67
68
if (!newPart) {
69
newPart = this.tryToParseDynamicVariable(message.slice(i), i, new Position(lineNumber, column), references);
70
}
71
}
72
73
if (newPart) {
74
if (i !== 0) {
75
// Insert a part for all the text we passed over, then insert the new parsed part
76
const previousPart = parts.at(-1);
77
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
78
const previousPartEditorRangeEndLine = previousPart?.editorRange.endLineNumber ?? 1;
79
const previousPartEditorRangeEndCol = previousPart?.editorRange.endColumn ?? 1;
80
parts.push(new ChatRequestTextPart(
81
new OffsetRange(previousPartEnd, i),
82
new Range(previousPartEditorRangeEndLine, previousPartEditorRangeEndCol, lineNumber, column),
83
message.slice(previousPartEnd, i)));
84
}
85
86
parts.push(newPart);
87
}
88
89
if (char === '\n') {
90
lineNumber++;
91
column = 1;
92
} else {
93
column++;
94
}
95
}
96
97
const lastPart = parts.at(-1);
98
const lastPartEnd = lastPart?.range.endExclusive ?? 0;
99
if (lastPartEnd < message.length) {
100
parts.push(new ChatRequestTextPart(
101
new OffsetRange(lastPartEnd, message.length),
102
new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column),
103
message.slice(lastPartEnd, message.length)));
104
}
105
106
return {
107
parts,
108
text: message,
109
};
110
}
111
112
private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: Array<IParsedChatRequestPart>, location: ChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | undefined {
113
const nextAgentMatch = message.match(agentReg);
114
if (!nextAgentMatch) {
115
return;
116
}
117
118
const [full, name] = nextAgentMatch;
119
const agentRange = new OffsetRange(offset, offset + full.length);
120
const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
121
122
let agents = this.agentService.getAgentsByName(name);
123
if (!agents.length) {
124
const fqAgent = this.agentService.getAgentByFullyQualifiedId(name);
125
if (fqAgent) {
126
agents = [fqAgent];
127
}
128
}
129
130
// 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 the
131
// context and we use that one.
132
const agent = agents.length > 1 && context?.selectedAgent ?
133
context.selectedAgent :
134
agents.find((a) => a.locations.includes(location));
135
if (!agent) {
136
return;
137
}
138
139
if (context?.mode && !agent.modes.includes(context.mode)) {
140
return;
141
}
142
143
if (parts.some(p => p instanceof ChatRequestAgentPart)) {
144
// Only one agent allowed
145
return;
146
}
147
148
// The agent must come first
149
if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart))) {
150
return;
151
}
152
153
const previousPart = parts.at(-1);
154
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
155
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
156
if (textSincePreviousPart.trim() !== '') {
157
return;
158
}
159
160
return new ChatRequestAgentPart(agentRange, agentEditorRange, agent);
161
}
162
163
private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, toolsByName: ReadonlyMap<string, IToolData>, toolSetsByName: ReadonlyMap<string, ToolSet>): ChatRequestToolPart | ChatRequestToolSetPart | undefined {
164
const nextVariableMatch = message.match(variableReg);
165
if (!nextVariableMatch) {
166
return;
167
}
168
169
const [full, name] = nextVariableMatch;
170
const varRange = new OffsetRange(offset, offset + full.length);
171
const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
172
173
const tool = toolsByName.get(name);
174
if (tool) {
175
return new ChatRequestToolPart(varRange, varEditorRange, name, tool.id, tool.displayName, tool.icon);
176
}
177
178
const toolset = toolSetsByName.get(name);
179
if (toolset) {
180
const value = Array.from(toolset.getTools()).map(t => new ChatRequestToolPart(varRange, varEditorRange, t.toolReferenceName ?? t.displayName, t.id, t.displayName, t.icon).toVariableEntry());
181
return new ChatRequestToolSetPart(varRange, varEditorRange, toolset.id, toolset.referenceName, toolset.icon, value);
182
}
183
184
return;
185
}
186
187
private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray<IParsedChatRequestPart>, location: ChatAgentLocation, context?: IChatParserContext): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | ChatRequestSlashPromptPart | undefined {
188
const nextSlashMatch = remainingMessage.match(slashReg);
189
if (!nextSlashMatch) {
190
return;
191
}
192
193
if (parts.some(p => !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart && p.text.trim() === ''))) {
194
// no other part than agent or non-whitespace text allowed: that also means no other slash command
195
return;
196
}
197
198
// only whitespace after the last part
199
const previousPart = parts.at(-1);
200
const previousPartEnd = previousPart?.range.endExclusive ?? 0;
201
const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset);
202
if (textSincePreviousPart.trim() !== '') {
203
return;
204
}
205
206
const [full, command] = nextSlashMatch;
207
const slashRange = new OffsetRange(offset, offset + full.length);
208
const slashEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length);
209
210
const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart)?.agent ??
211
(context?.forcedAgent ? context.forcedAgent : undefined);
212
if (usedAgent) {
213
const subCommand = usedAgent.slashCommands.find(c => c.name === command);
214
if (subCommand) {
215
// Valid agent subcommand
216
return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);
217
}
218
} else {
219
const slashCommands = this.slashCommandService.getCommands(location, context?.mode ?? ChatModeKind.Ask);
220
const slashCommand = slashCommands.find(c => c.command === command);
221
if (slashCommand) {
222
// Valid standalone slash command
223
return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand);
224
} else {
225
// check for with default agent for this location
226
const defaultAgent = this.agentService.getDefaultAgent(location, context?.mode);
227
const subCommand = defaultAgent?.slashCommands.find(c => c.name === command);
228
if (subCommand) {
229
// Valid default agent subcommand
230
return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);
231
}
232
}
233
234
// if there's no agent, asume it is a prompt slash command
235
const isPromptCommand = this.promptsService.isValidSlashCommandName(command);
236
if (isPromptCommand) {
237
return new ChatRequestSlashPromptPart(slashRange, slashEditorRange, command);
238
}
239
}
240
return;
241
}
242
243
private tryToParseDynamicVariable(message: string, offset: number, position: IPosition, references: ReadonlyArray<IDynamicVariable>): ChatRequestDynamicVariablePart | undefined {
244
const refAtThisPosition = references.find(r =>
245
r.range.startLineNumber === position.lineNumber &&
246
r.range.startColumn === position.column);
247
if (refAtThisPosition) {
248
const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn;
249
const text = message.substring(0, length);
250
const range = new OffsetRange(offset, offset + length);
251
return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data, refAtThisPosition.fullName, refAtThisPosition.icon, refAtThisPosition.isFile, refAtThisPosition.isDirectory);
252
}
253
254
return;
255
}
256
}
257
258