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