@theia/ai-chat 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/lib/browser/ai-chat-frontend-module.d.ts +4 -0
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -0
- package/lib/browser/ai-chat-frontend-module.js +51 -0
- package/lib/browser/ai-chat-frontend-module.js.map +1 -0
- package/lib/browser/ai-chat-preferences.d.ts +4 -0
- package/lib/browser/ai-chat-preferences.d.ts.map +1 -0
- package/lib/browser/ai-chat-preferences.js +32 -0
- package/lib/browser/ai-chat-preferences.js.map +1 -0
- package/lib/browser/frontend-chat-service.d.ts +8 -0
- package/lib/browser/frontend-chat-service.d.ts.map +1 -0
- package/lib/browser/frontend-chat-service.js +67 -0
- package/lib/browser/frontend-chat-service.js.map +1 -0
- package/lib/common/chat-agent-service.d.ts +34 -0
- package/lib/common/chat-agent-service.d.ts.map +1 -0
- package/lib/common/chat-agent-service.js +75 -0
- package/lib/common/chat-agent-service.js.map +1 -0
- package/lib/common/chat-agents-variable-contribution.d.ts +17 -0
- package/lib/common/chat-agents-variable-contribution.d.ts.map +1 -0
- package/lib/common/chat-agents-variable-contribution.js +51 -0
- package/lib/common/chat-agents-variable-contribution.js.map +1 -0
- package/lib/common/chat-agents.d.ts +86 -0
- package/lib/common/chat-agents.d.ts.map +1 -0
- package/lib/common/chat-agents.js +307 -0
- package/lib/common/chat-agents.js.map +1 -0
- package/lib/common/chat-model.d.ts +319 -0
- package/lib/common/chat-model.d.ts.map +1 -0
- package/lib/common/chat-model.js +527 -0
- package/lib/common/chat-model.js.map +1 -0
- package/lib/common/chat-request-parser.d.ts +20 -0
- package/lib/common/chat-request-parser.d.ts.map +1 -0
- package/lib/common/chat-request-parser.js +158 -0
- package/lib/common/chat-request-parser.js.map +1 -0
- package/lib/common/chat-request-parser.spec.d.ts +2 -0
- package/lib/common/chat-request-parser.spec.d.ts.map +1 -0
- package/lib/common/chat-request-parser.spec.js +108 -0
- package/lib/common/chat-request-parser.spec.js.map +1 -0
- package/lib/common/chat-service.d.ts +72 -0
- package/lib/common/chat-service.d.ts.map +1 -0
- package/lib/common/chat-service.js +170 -0
- package/lib/common/chat-service.js.map +1 -0
- package/lib/common/command-chat-agents.d.ts +33 -0
- package/lib/common/command-chat-agents.d.ts.map +1 -0
- package/lib/common/command-chat-agents.js +327 -0
- package/lib/common/command-chat-agents.js.map +1 -0
- package/lib/common/index.d.ts +10 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +28 -0
- package/lib/common/index.js.map +1 -0
- package/lib/common/orchestrator-chat-agent.d.ts +22 -0
- package/lib/common/orchestrator-chat-agent.d.ts.map +1 -0
- package/lib/common/orchestrator-chat-agent.js +140 -0
- package/lib/common/orchestrator-chat-agent.js.map +1 -0
- package/lib/common/parsed-chat-request.d.ts +66 -0
- package/lib/common/parsed-chat-request.d.ts.map +1 -0
- package/lib/common/parsed-chat-request.js +83 -0
- package/lib/common/parsed-chat-request.js.map +1 -0
- package/lib/common/universal-chat-agent.d.ts +15 -0
- package/lib/common/universal-chat-agent.d.ts.map +1 -0
- package/lib/common/universal-chat-agent.js +102 -0
- package/lib/common/universal-chat-agent.js.map +1 -0
- package/package.json +54 -0
- package/src/browser/ai-chat-frontend-module.ts +66 -0
- package/src/browser/ai-chat-preferences.ts +32 -0
- package/src/browser/frontend-chat-service.ts +66 -0
- package/src/common/chat-agent-service.ts +85 -0
- package/src/common/chat-agents-variable-contribution.ts +81 -0
- package/src/common/chat-agents.ts +384 -0
- package/src/common/chat-model.ts +776 -0
- package/src/common/chat-request-parser.spec.ts +120 -0
- package/src/common/chat-request-parser.ts +220 -0
- package/src/common/chat-service.ts +236 -0
- package/src/common/command-chat-agents.ts +352 -0
- package/src/common/index.ts +24 -0
- package/src/common/orchestrator-chat-agent.ts +151 -0
- package/src/common/parsed-chat-request.ts +112 -0
- package/src/common/universal-chat-agent.ts +109 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2024 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import * as sinon from 'sinon';
|
|
18
|
+
import { ChatAgentServiceImpl } from './chat-agent-service';
|
|
19
|
+
import { ChatRequestParserImpl } from './chat-request-parser';
|
|
20
|
+
import { ChatAgentLocation } from './chat-agents';
|
|
21
|
+
import { ChatRequest } from './chat-model';
|
|
22
|
+
import { expect } from 'chai';
|
|
23
|
+
import { DefaultAIVariableService, ToolInvocationRegistry, ToolInvocationRegistryImpl } from '@theia/ai-core';
|
|
24
|
+
|
|
25
|
+
describe('ChatRequestParserImpl', () => {
|
|
26
|
+
const chatAgentService = sinon.createStubInstance(ChatAgentServiceImpl);
|
|
27
|
+
const variableService = sinon.createStubInstance(DefaultAIVariableService);
|
|
28
|
+
const toolInvocationRegistry: ToolInvocationRegistry = sinon.createStubInstance(ToolInvocationRegistryImpl);
|
|
29
|
+
const parser = new ChatRequestParserImpl(chatAgentService, variableService, toolInvocationRegistry);
|
|
30
|
+
|
|
31
|
+
it('parses simple text', () => {
|
|
32
|
+
const req: ChatRequest = {
|
|
33
|
+
text: 'What is the best pizza topping?'
|
|
34
|
+
};
|
|
35
|
+
const result = parser.parseChatRequest(req, ChatAgentLocation.Panel);
|
|
36
|
+
expect(result.parts).to.deep.contain({
|
|
37
|
+
text: 'What is the best pizza topping?',
|
|
38
|
+
range: { start: 0, endExclusive: 31 }
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('parses text with variable name', () => {
|
|
43
|
+
const req: ChatRequest = {
|
|
44
|
+
text: 'What is the #best pizza topping?'
|
|
45
|
+
};
|
|
46
|
+
const result = parser.parseChatRequest(req, ChatAgentLocation.Panel);
|
|
47
|
+
expect(result).to.deep.contain({
|
|
48
|
+
parts: [{
|
|
49
|
+
text: 'What is the ',
|
|
50
|
+
range: { start: 0, endExclusive: 12 }
|
|
51
|
+
}, {
|
|
52
|
+
variableName: 'best',
|
|
53
|
+
variableArg: undefined,
|
|
54
|
+
range: { start: 12, endExclusive: 17 }
|
|
55
|
+
}, {
|
|
56
|
+
text: ' pizza topping?',
|
|
57
|
+
range: { start: 17, endExclusive: 32 }
|
|
58
|
+
}]
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('parses text with variable name with argument', () => {
|
|
63
|
+
const req: ChatRequest = {
|
|
64
|
+
text: 'What is the #best:by-poll pizza topping?'
|
|
65
|
+
};
|
|
66
|
+
const result = parser.parseChatRequest(req, ChatAgentLocation.Panel);
|
|
67
|
+
expect(result).to.deep.contain({
|
|
68
|
+
parts: [{
|
|
69
|
+
text: 'What is the ',
|
|
70
|
+
range: { start: 0, endExclusive: 12 }
|
|
71
|
+
}, {
|
|
72
|
+
variableName: 'best',
|
|
73
|
+
variableArg: 'by-poll',
|
|
74
|
+
range: { start: 12, endExclusive: 25 }
|
|
75
|
+
}, {
|
|
76
|
+
text: ' pizza topping?',
|
|
77
|
+
range: { start: 25, endExclusive: 40 }
|
|
78
|
+
}]
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('parses text with variable name with numeric argument', () => {
|
|
83
|
+
const req: ChatRequest = {
|
|
84
|
+
text: '#size-class:2'
|
|
85
|
+
};
|
|
86
|
+
const result = parser.parseChatRequest(req, ChatAgentLocation.Panel);
|
|
87
|
+
expect(result.parts[0]).to.contain(
|
|
88
|
+
{
|
|
89
|
+
variableName: 'size-class',
|
|
90
|
+
variableArg: '2'
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('parses text with variable name with POSIX path argument', () => {
|
|
96
|
+
const req: ChatRequest = {
|
|
97
|
+
text: '#file:/path/to/file.ext'
|
|
98
|
+
};
|
|
99
|
+
const result = parser.parseChatRequest(req, ChatAgentLocation.Panel);
|
|
100
|
+
expect(result.parts[0]).to.contain(
|
|
101
|
+
{
|
|
102
|
+
variableName: 'file',
|
|
103
|
+
variableArg: '/path/to/file.ext'
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('parses text with variable name with Win32 path argument', () => {
|
|
109
|
+
const req: ChatRequest = {
|
|
110
|
+
text: '#file:c:\\path\\to\\file.ext'
|
|
111
|
+
};
|
|
112
|
+
const result = parser.parseChatRequest(req, ChatAgentLocation.Panel);
|
|
113
|
+
expect(result.parts[0]).to.contain(
|
|
114
|
+
{
|
|
115
|
+
variableName: 'file',
|
|
116
|
+
variableArg: 'c:\\path\\to\\file.ext'
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2024 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
/*---------------------------------------------------------------------------------------------
|
|
17
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
18
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
19
|
+
*--------------------------------------------------------------------------------------------*/
|
|
20
|
+
// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatRequestParser.ts
|
|
21
|
+
|
|
22
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
23
|
+
import { ChatAgentService } from './chat-agent-service';
|
|
24
|
+
import { ChatAgentLocation } from './chat-agents';
|
|
25
|
+
import { ChatRequest } from './chat-model';
|
|
26
|
+
import {
|
|
27
|
+
chatAgentLeader,
|
|
28
|
+
chatFunctionLeader,
|
|
29
|
+
ParsedChatRequestAgentPart,
|
|
30
|
+
ParsedChatRequestFunctionPart,
|
|
31
|
+
ParsedChatRequestTextPart,
|
|
32
|
+
ParsedChatRequestVariablePart,
|
|
33
|
+
chatVariableLeader,
|
|
34
|
+
OffsetRange,
|
|
35
|
+
ParsedChatRequest,
|
|
36
|
+
ParsedChatRequestPart,
|
|
37
|
+
} from './parsed-chat-request';
|
|
38
|
+
import { AIVariable, AIVariableService, ToolInvocationRegistry, ToolRequest } from '@theia/ai-core';
|
|
39
|
+
|
|
40
|
+
const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent
|
|
41
|
+
const functionReg = /^~([\w_\-\.]+)(?=(\s|$|\b))/i; // A ~ tool function
|
|
42
|
+
const variableReg = /^#([\w_\-]+)(?::([\w_\-_\/\\.:]+))?(?=(\s|$|\b))/i; // A #-variable with an optional : arg (#file:workspace/path/name.ext)
|
|
43
|
+
|
|
44
|
+
export const ChatRequestParser = Symbol('ChatRequestParser');
|
|
45
|
+
export interface ChatRequestParser {
|
|
46
|
+
parseChatRequest(request: ChatRequest, location: ChatAgentLocation): ParsedChatRequest;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function offsetRange(start: number, endExclusive: number): OffsetRange {
|
|
50
|
+
if (start > endExclusive) {
|
|
51
|
+
throw new Error(`Invalid range: start=${start} endExclusive=${endExclusive}`);
|
|
52
|
+
}
|
|
53
|
+
return { start, endExclusive };
|
|
54
|
+
}
|
|
55
|
+
@injectable()
|
|
56
|
+
export class ChatRequestParserImpl {
|
|
57
|
+
constructor(
|
|
58
|
+
@inject(ChatAgentService) private readonly agentService: ChatAgentService,
|
|
59
|
+
@inject(AIVariableService) private readonly variableService: AIVariableService,
|
|
60
|
+
@inject(ToolInvocationRegistry) private readonly toolInvocationRegistry: ToolInvocationRegistry
|
|
61
|
+
) { }
|
|
62
|
+
|
|
63
|
+
parseChatRequest(request: ChatRequest, location: ChatAgentLocation): ParsedChatRequest {
|
|
64
|
+
const parts: ParsedChatRequestPart[] = [];
|
|
65
|
+
const variables = new Map<string, AIVariable>();
|
|
66
|
+
const toolRequests = new Map<string, ToolRequest>();
|
|
67
|
+
const message = request.text;
|
|
68
|
+
for (let i = 0; i < message.length; i++) {
|
|
69
|
+
const previousChar = message.charAt(i - 1);
|
|
70
|
+
const char = message.charAt(i);
|
|
71
|
+
let newPart: ParsedChatRequestPart | undefined;
|
|
72
|
+
|
|
73
|
+
if (previousChar.match(/\s/) || i === 0) {
|
|
74
|
+
if (char === chatFunctionLeader) {
|
|
75
|
+
const functionPart = this.tryParseFunction(
|
|
76
|
+
message.slice(i),
|
|
77
|
+
i
|
|
78
|
+
);
|
|
79
|
+
newPart = functionPart;
|
|
80
|
+
if (functionPart) {
|
|
81
|
+
toolRequests.set(functionPart.toolRequest.id, functionPart.toolRequest);
|
|
82
|
+
}
|
|
83
|
+
} else if (char === chatVariableLeader) {
|
|
84
|
+
const variablePart = this.tryToParseVariable(
|
|
85
|
+
message.slice(i),
|
|
86
|
+
i,
|
|
87
|
+
parts
|
|
88
|
+
);
|
|
89
|
+
newPart = variablePart;
|
|
90
|
+
if (variablePart) {
|
|
91
|
+
const variable = this.variableService.getVariable(variablePart.variableName);
|
|
92
|
+
if (variable) {
|
|
93
|
+
variables.set(variable.name, variable);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (char === chatAgentLeader) {
|
|
97
|
+
newPart = this.tryToParseAgent(
|
|
98
|
+
message.slice(i),
|
|
99
|
+
i,
|
|
100
|
+
parts,
|
|
101
|
+
location
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (newPart) {
|
|
107
|
+
if (i !== 0) {
|
|
108
|
+
// Insert a part for all the text we passed over, then insert the new parsed part
|
|
109
|
+
const previousPart = parts.at(-1);
|
|
110
|
+
const previousPartEnd =
|
|
111
|
+
previousPart?.range.endExclusive ?? 0;
|
|
112
|
+
parts.push(
|
|
113
|
+
new ParsedChatRequestTextPart(
|
|
114
|
+
offsetRange(previousPartEnd, i),
|
|
115
|
+
message.slice(previousPartEnd, i)
|
|
116
|
+
)
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
parts.push(newPart);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const lastPart = parts.at(-1);
|
|
125
|
+
const lastPartEnd = lastPart?.range.endExclusive ?? 0;
|
|
126
|
+
if (lastPartEnd < message.length) {
|
|
127
|
+
parts.push(
|
|
128
|
+
new ParsedChatRequestTextPart(
|
|
129
|
+
offsetRange(lastPartEnd, message.length),
|
|
130
|
+
message.slice(lastPartEnd, message.length)
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { request, parts, toolRequests, variables };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private tryToParseAgent(
|
|
139
|
+
message: string,
|
|
140
|
+
offset: number,
|
|
141
|
+
parts: ReadonlyArray<ParsedChatRequestPart>,
|
|
142
|
+
location: ChatAgentLocation
|
|
143
|
+
): ParsedChatRequestAgentPart | ParsedChatRequestVariablePart | undefined {
|
|
144
|
+
const nextAgentMatch = message.match(agentReg);
|
|
145
|
+
if (!nextAgentMatch) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const [full, name] = nextAgentMatch;
|
|
150
|
+
const agentRange = offsetRange(offset, offset + full.length);
|
|
151
|
+
|
|
152
|
+
let agents = this.agentService.getAgents().filter(a => a.name === name);
|
|
153
|
+
if (!agents.length) {
|
|
154
|
+
const fqAgent = this.agentService.getAgent(name);
|
|
155
|
+
if (fqAgent) {
|
|
156
|
+
agents = [fqAgent];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 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
|
|
161
|
+
// context and we use that one. Otherwise just pick the first.
|
|
162
|
+
const agent = agents[0];
|
|
163
|
+
if (!agent || !agent.locations.includes(location)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (parts.some(p => p instanceof ParsedChatRequestAgentPart)) {
|
|
168
|
+
// Only one agent allowed
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The agent must come first
|
|
173
|
+
if (
|
|
174
|
+
parts.some(
|
|
175
|
+
p =>
|
|
176
|
+
(p instanceof ParsedChatRequestTextPart &&
|
|
177
|
+
p.text.trim() !== '') ||
|
|
178
|
+
!(p instanceof ParsedChatRequestAgentPart)
|
|
179
|
+
)
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return new ParsedChatRequestAgentPart(agentRange, agent.id, agent.name);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private tryToParseVariable(
|
|
188
|
+
message: string,
|
|
189
|
+
offset: number,
|
|
190
|
+
_parts: ReadonlyArray<ParsedChatRequestPart>
|
|
191
|
+
): ParsedChatRequestVariablePart | undefined {
|
|
192
|
+
const nextVariableMatch = message.match(variableReg);
|
|
193
|
+
if (!nextVariableMatch) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const [full, name] = nextVariableMatch;
|
|
198
|
+
const variableArg = nextVariableMatch[2];
|
|
199
|
+
const varRange = offsetRange(offset, offset + full.length);
|
|
200
|
+
|
|
201
|
+
return new ParsedChatRequestVariablePart(varRange, name, variableArg);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private tryParseFunction(message: string, offset: number): ParsedChatRequestFunctionPart | undefined {
|
|
205
|
+
const nextFunctionMatch = message.match(functionReg);
|
|
206
|
+
if (!nextFunctionMatch) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const [full, id] = nextFunctionMatch;
|
|
211
|
+
|
|
212
|
+
const maybeToolRequest = this.toolInvocationRegistry.getFunction(id);
|
|
213
|
+
if (!maybeToolRequest) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const functionRange = offsetRange(offset, offset + full.length);
|
|
218
|
+
return new ParsedChatRequestFunctionPart(functionRange, maybeToolRequest);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2024 EclipseSource GmbH.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
/*---------------------------------------------------------------------------------------------
|
|
17
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
18
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
19
|
+
*--------------------------------------------------------------------------------------------*/
|
|
20
|
+
// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatService.ts
|
|
21
|
+
|
|
22
|
+
import { inject, injectable, optional } from '@theia/core/shared/inversify';
|
|
23
|
+
import {
|
|
24
|
+
ChatModel,
|
|
25
|
+
ChatModelImpl,
|
|
26
|
+
ChatRequest,
|
|
27
|
+
ChatRequestModel,
|
|
28
|
+
ChatResponseModel,
|
|
29
|
+
ErrorChatResponseModelImpl,
|
|
30
|
+
} from './chat-model';
|
|
31
|
+
import { ChatAgentService } from './chat-agent-service';
|
|
32
|
+
import { Emitter, ILogger, generateUuid } from '@theia/core';
|
|
33
|
+
import { ChatRequestParser } from './chat-request-parser';
|
|
34
|
+
import { ChatAgent, ChatAgentLocation } from './chat-agents';
|
|
35
|
+
import { ParsedChatRequestAgentPart, ParsedChatRequestVariablePart, ParsedChatRequest } from './parsed-chat-request';
|
|
36
|
+
import { AIVariableService } from '@theia/ai-core';
|
|
37
|
+
import { Event } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
38
|
+
|
|
39
|
+
export interface ChatRequestInvocation {
|
|
40
|
+
/**
|
|
41
|
+
* Promise which completes once the request preprocessing is complete.
|
|
42
|
+
*/
|
|
43
|
+
requestCompleted: Promise<ChatRequestModel>;
|
|
44
|
+
/**
|
|
45
|
+
* Promise which completes once a response is expected to arrive.
|
|
46
|
+
*/
|
|
47
|
+
responseCreated: Promise<ChatResponseModel>;
|
|
48
|
+
/**
|
|
49
|
+
* Promise which completes once the response is complete.
|
|
50
|
+
*/
|
|
51
|
+
responseCompleted: Promise<ChatResponseModel>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ChatSession {
|
|
55
|
+
id: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
model: ChatModel;
|
|
58
|
+
isActive: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ActiveSessionChangedEvent {
|
|
62
|
+
sessionId: string | undefined;
|
|
63
|
+
focus?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SessionOptions {
|
|
67
|
+
focus?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const DefaultChatAgentId = Symbol('DefaultChatAgentId');
|
|
71
|
+
export interface DefaultChatAgentId {
|
|
72
|
+
id: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const ChatService = Symbol('ChatService');
|
|
76
|
+
export interface ChatService {
|
|
77
|
+
onActiveSessionChanged: Event<ActiveSessionChangedEvent>
|
|
78
|
+
|
|
79
|
+
getSession(id: string): ChatSession | undefined;
|
|
80
|
+
getSessions(): ChatSession[];
|
|
81
|
+
createSession(location?: ChatAgentLocation, options?: SessionOptions): ChatSession;
|
|
82
|
+
deleteSession(sessionId: string): void;
|
|
83
|
+
setActiveSession(sessionId: string, options?: SessionOptions): void;
|
|
84
|
+
|
|
85
|
+
sendRequest(
|
|
86
|
+
sessionId: string,
|
|
87
|
+
request: ChatRequest
|
|
88
|
+
): Promise<ChatRequestInvocation | undefined>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ChatSessionInternal extends ChatSession {
|
|
92
|
+
model: ChatModelImpl;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@injectable()
|
|
96
|
+
export class ChatServiceImpl implements ChatService {
|
|
97
|
+
protected readonly onActiveSessionChangedEmitter = new Emitter<ActiveSessionChangedEvent>();
|
|
98
|
+
onActiveSessionChanged = this.onActiveSessionChangedEmitter.event;
|
|
99
|
+
|
|
100
|
+
@inject(ChatAgentService)
|
|
101
|
+
protected chatAgentService: ChatAgentService;
|
|
102
|
+
|
|
103
|
+
@inject(DefaultChatAgentId) @optional()
|
|
104
|
+
protected defaultChatAgentId: DefaultChatAgentId | undefined;
|
|
105
|
+
|
|
106
|
+
@inject(ChatRequestParser)
|
|
107
|
+
protected chatRequestParser: ChatRequestParser;
|
|
108
|
+
|
|
109
|
+
@inject(AIVariableService)
|
|
110
|
+
protected variableService: AIVariableService;
|
|
111
|
+
|
|
112
|
+
@inject(ILogger)
|
|
113
|
+
protected logger: ILogger;
|
|
114
|
+
|
|
115
|
+
protected _sessions: ChatSessionInternal[] = [];
|
|
116
|
+
|
|
117
|
+
getSessions(): ChatSessionInternal[] {
|
|
118
|
+
return [...this._sessions];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
getSession(id: string): ChatSessionInternal | undefined {
|
|
122
|
+
return this._sessions.find(session => session.id === id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
createSession(location = ChatAgentLocation.Panel, options?: SessionOptions): ChatSession {
|
|
126
|
+
const model = new ChatModelImpl(location);
|
|
127
|
+
const session: ChatSessionInternal = {
|
|
128
|
+
id: model.id,
|
|
129
|
+
model,
|
|
130
|
+
isActive: true
|
|
131
|
+
};
|
|
132
|
+
this._sessions.push(session);
|
|
133
|
+
this.setActiveSession(session.id, options);
|
|
134
|
+
return session;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
deleteSession(sessionId: string): void {
|
|
138
|
+
// If the removed session is the active one, set the newest one as active
|
|
139
|
+
if (this.getSession(sessionId)?.isActive) {
|
|
140
|
+
this.setActiveSession(this._sessions[this._sessions.length - 1]?.id);
|
|
141
|
+
}
|
|
142
|
+
this._sessions = this._sessions.filter(item => item.id !== sessionId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setActiveSession(sessionId: string | undefined, options?: SessionOptions): void {
|
|
146
|
+
this._sessions.forEach(session => {
|
|
147
|
+
session.isActive = session.id === sessionId;
|
|
148
|
+
});
|
|
149
|
+
this.onActiveSessionChangedEmitter.fire({ sessionId: sessionId, ...options });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async sendRequest(
|
|
153
|
+
sessionId: string,
|
|
154
|
+
request: ChatRequest
|
|
155
|
+
): Promise<ChatRequestInvocation | undefined> {
|
|
156
|
+
const session = this.getSession(sessionId);
|
|
157
|
+
if (!session) {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
session.title = request.text;
|
|
161
|
+
|
|
162
|
+
const parsedRequest = this.chatRequestParser.parseChatRequest(request, session.model.location);
|
|
163
|
+
|
|
164
|
+
const agent = this.getAgent(parsedRequest);
|
|
165
|
+
if (agent === undefined) {
|
|
166
|
+
const error = 'No ChatAgents available to handle request!';
|
|
167
|
+
this.logger.error(error);
|
|
168
|
+
const chatResponseModel = new ErrorChatResponseModelImpl(generateUuid(), new Error(error));
|
|
169
|
+
return {
|
|
170
|
+
requestCompleted: Promise.reject(error),
|
|
171
|
+
responseCreated: Promise.reject(error),
|
|
172
|
+
responseCompleted: Promise.resolve(chatResponseModel),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const requestModel = session.model.addRequest(parsedRequest, agent?.id);
|
|
176
|
+
|
|
177
|
+
for (const part of parsedRequest.parts) {
|
|
178
|
+
if (part instanceof ParsedChatRequestVariablePart) {
|
|
179
|
+
const resolvedVariable = await this.variableService.resolveVariable(
|
|
180
|
+
{ variable: part.variableName, arg: part.variableArg },
|
|
181
|
+
{ request, model: session }
|
|
182
|
+
);
|
|
183
|
+
if (resolvedVariable) {
|
|
184
|
+
part.resolution = resolvedVariable;
|
|
185
|
+
} else {
|
|
186
|
+
this.logger.warn(`Failed to resolve variable ${part.variableName} for ${session.model.location}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let resolveResponseCreated: (responseModel: ChatResponseModel) => void;
|
|
192
|
+
let resolveResponseCompleted: (responseModel: ChatResponseModel) => void;
|
|
193
|
+
const invocation: ChatRequestInvocation = {
|
|
194
|
+
requestCompleted: Promise.resolve(requestModel),
|
|
195
|
+
responseCreated: new Promise(resolve => {
|
|
196
|
+
resolveResponseCreated = resolve;
|
|
197
|
+
}),
|
|
198
|
+
responseCompleted: new Promise(resolve => {
|
|
199
|
+
resolveResponseCompleted = resolve;
|
|
200
|
+
}),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
resolveResponseCreated!(requestModel.response);
|
|
204
|
+
requestModel.response.onDidChange(() => {
|
|
205
|
+
if (requestModel.response.isComplete) {
|
|
206
|
+
resolveResponseCompleted!(requestModel.response);
|
|
207
|
+
}
|
|
208
|
+
if (requestModel.response.isError) {
|
|
209
|
+
resolveResponseCompleted!(requestModel.response);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (agent) {
|
|
214
|
+
agent.invoke(requestModel).catch(error => requestModel.response.error(error));
|
|
215
|
+
} else {
|
|
216
|
+
this.logger.error('No ChatAgents available to handle request!', requestModel);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return invocation;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
protected getAgent(parsedRequest: ParsedChatRequest): ChatAgent | undefined {
|
|
223
|
+
const agentPart = this.getMentionedAgent(parsedRequest);
|
|
224
|
+
if (agentPart) {
|
|
225
|
+
return this.chatAgentService.getAgent(agentPart.agentId);
|
|
226
|
+
}
|
|
227
|
+
if (this.defaultChatAgentId) {
|
|
228
|
+
return this.chatAgentService.getAgent(this.defaultChatAgentId.id);
|
|
229
|
+
}
|
|
230
|
+
return this.chatAgentService.getAgents()[0] ?? undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
protected getMentionedAgent(parsedRequest: ParsedChatRequest): ParsedChatRequestAgentPart | undefined {
|
|
234
|
+
return parsedRequest.parts.find(p => p instanceof ParsedChatRequestAgentPart) as ParsedChatRequestAgentPart | undefined;
|
|
235
|
+
}
|
|
236
|
+
}
|