@theia/ai-core 1.63.0-next.24 → 1.63.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/lib/browser/agent-completion-notification-service.d.ts +69 -0
- package/lib/browser/agent-completion-notification-service.d.ts.map +1 -0
- package/lib/browser/agent-completion-notification-service.js +187 -0
- package/lib/browser/agent-completion-notification-service.js.map +1 -0
- package/lib/browser/agent-preferences.d.ts.map +1 -1
- package/lib/browser/agent-preferences.js +11 -0
- package/lib/browser/agent-preferences.js.map +1 -1
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +9 -1
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-core-preferences.d.ts +5 -0
- package/lib/browser/ai-core-preferences.d.ts.map +1 -1
- package/lib/browser/ai-core-preferences.js +22 -1
- package/lib/browser/ai-core-preferences.js.map +1 -1
- package/lib/browser/frontend-language-model-registry.d.ts +5 -5
- package/lib/browser/frontend-language-model-registry.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-registry.js +12 -5
- package/lib/browser/frontend-language-model-registry.js.map +1 -1
- package/lib/browser/frontend-language-model-registry.spec.d.ts +2 -0
- package/lib/browser/frontend-language-model-registry.spec.d.ts.map +1 -0
- package/lib/browser/frontend-language-model-registry.spec.js +248 -0
- package/lib/browser/frontend-language-model-registry.spec.js.map +1 -0
- package/lib/browser/frontend-variable-service.d.ts +12 -0
- package/lib/browser/frontend-variable-service.d.ts.map +1 -1
- package/lib/browser/frontend-variable-service.js +23 -0
- package/lib/browser/frontend-variable-service.js.map +1 -1
- package/lib/browser/index.d.ts +5 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +5 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/open-editors-variable-contribution.d.ts +17 -0
- package/lib/browser/open-editors-variable-contribution.d.ts.map +1 -0
- package/lib/browser/open-editors-variable-contribution.js +84 -0
- package/lib/browser/open-editors-variable-contribution.js.map +1 -0
- package/lib/browser/os-notification-service.d.ts +96 -0
- package/lib/browser/os-notification-service.d.ts.map +1 -0
- package/lib/browser/os-notification-service.js +222 -0
- package/lib/browser/os-notification-service.js.map +1 -0
- package/lib/browser/window-blink-service.d.ts +41 -0
- package/lib/browser/window-blink-service.d.ts.map +1 -0
- package/lib/browser/window-blink-service.js +174 -0
- package/lib/browser/window-blink-service.js.map +1 -0
- package/lib/common/configurable-in-memory-resources.d.ts +2 -0
- package/lib/common/configurable-in-memory-resources.d.ts.map +1 -1
- package/lib/common/configurable-in-memory-resources.js +7 -0
- package/lib/common/configurable-in-memory-resources.js.map +1 -1
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-delegate.d.ts +2 -2
- package/lib/common/language-model-delegate.d.ts.map +1 -1
- package/lib/common/language-model-delegate.js.map +1 -1
- package/lib/common/language-model.d.ts +44 -4
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +16 -1
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/notification-types.d.ts +7 -0
- package/lib/common/notification-types.d.ts.map +1 -0
- package/lib/common/notification-types.js +29 -0
- package/lib/common/notification-types.js.map +1 -0
- package/lib/common/prompt-service.d.ts +14 -4
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +78 -9
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +3 -0
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/settings-service.d.ts +6 -0
- package/lib/common/settings-service.d.ts.map +1 -1
- package/lib/common/settings-service.js.map +1 -1
- package/lib/common/variable-service.d.ts +2 -0
- package/lib/common/variable-service.d.ts.map +1 -1
- package/lib/common/variable-service.js +10 -2
- package/lib/common/variable-service.js.map +1 -1
- package/package.json +10 -10
- package/src/browser/agent-completion-notification-service.ts +242 -0
- package/src/browser/agent-preferences.ts +14 -0
- package/src/browser/ai-core-frontend-module.ts +10 -0
- package/src/browser/ai-core-preferences.ts +30 -0
- package/src/browser/frontend-language-model-registry.spec.ts +298 -0
- package/src/browser/frontend-language-model-registry.ts +14 -8
- package/src/browser/frontend-variable-service.ts +36 -0
- package/src/browser/index.ts +5 -0
- package/src/browser/open-editors-variable-contribution.ts +88 -0
- package/src/browser/os-notification-service.ts +271 -0
- package/src/browser/window-blink-service.ts +195 -0
- package/src/common/configurable-in-memory-resources.ts +8 -0
- package/src/common/index.ts +1 -0
- package/src/common/language-model-delegate.ts +5 -2
- package/src/common/language-model.ts +32 -4
- package/src/common/notification-types.ts +31 -0
- package/src/common/prompt-service.spec.ts +4 -1
- package/src/common/prompt-service.ts +101 -12
- package/src/common/settings-service.ts +6 -0
- package/src/common/variable-service.ts +11 -1
|
@@ -18,11 +18,18 @@ import { nls } from '@theia/core';
|
|
|
18
18
|
import { PreferenceContribution, PreferenceProxy, PreferenceSchema } from '@theia/core/lib/browser';
|
|
19
19
|
import { PreferenceProxyFactory } from '@theia/core/lib/browser/preferences/injectable-preference-proxy';
|
|
20
20
|
import { interfaces } from '@theia/core/shared/inversify';
|
|
21
|
+
import {
|
|
22
|
+
NOTIFICATION_TYPES,
|
|
23
|
+
NOTIFICATION_TYPE_OFF,
|
|
24
|
+
NotificationType
|
|
25
|
+
} from '../common/notification-types';
|
|
21
26
|
|
|
22
27
|
export const AI_CORE_PREFERENCES_TITLE = nls.localize('theia/ai/core/prefs/title', '✨ AI Features [Alpha]');
|
|
23
28
|
export const PREFERENCE_NAME_ENABLE_AI = 'ai-features.AiEnable.enableAI';
|
|
24
29
|
export const PREFERENCE_NAME_PROMPT_TEMPLATES = 'ai-features.promptTemplates.promptTemplatesFolder';
|
|
25
30
|
export const PREFERENCE_NAME_REQUEST_SETTINGS = 'ai-features.modelSettings.requestSettings';
|
|
31
|
+
export const PREFERENCE_NAME_MAX_RETRIES = 'ai-features.modelSettings.maxRetries';
|
|
32
|
+
export const PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE = 'ai-features.notifications.default';
|
|
26
33
|
|
|
27
34
|
export const aiCorePreferenceSchema: PreferenceSchema = {
|
|
28
35
|
type: 'object',
|
|
@@ -121,13 +128,36 @@ export const aiCorePreferenceSchema: PreferenceSchema = {
|
|
|
121
128
|
additionalProperties: false
|
|
122
129
|
},
|
|
123
130
|
default: [],
|
|
131
|
+
},
|
|
132
|
+
[PREFERENCE_NAME_MAX_RETRIES]: {
|
|
133
|
+
title: nls.localize('theia/ai/core/maxRetries/title', 'Maximum Retries'),
|
|
134
|
+
markdownDescription: nls.localize('theia/ai/core/maxRetries/mdDescription',
|
|
135
|
+
'The maximum number of retry attempts when a request to an AI provider fails. A value of 0 means no retries.'),
|
|
136
|
+
type: 'number',
|
|
137
|
+
minimum: 0,
|
|
138
|
+
default: 3
|
|
139
|
+
},
|
|
140
|
+
[PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE]: {
|
|
141
|
+
title: nls.localize('theia/ai/core/defaultNotification/title', 'Default Notification Type'),
|
|
142
|
+
markdownDescription: nls.localize('theia/ai/core/defaultNotification/mdDescription',
|
|
143
|
+
'The default notification method used when an AI agent completes a task. Individual agents can override this setting.\n\
|
|
144
|
+
- `os-notification`: Show OS/system notifications\n\
|
|
145
|
+
- `message`: Show notifications in the status bar/message area\n\
|
|
146
|
+
- `blink`: Blink or highlight the UI\n\
|
|
147
|
+
- `off`: Disable all notifications'),
|
|
148
|
+
type: 'string',
|
|
149
|
+
enum: [...NOTIFICATION_TYPES],
|
|
150
|
+
default: NOTIFICATION_TYPE_OFF
|
|
124
151
|
}
|
|
125
152
|
}
|
|
126
153
|
};
|
|
154
|
+
|
|
127
155
|
export interface AICoreConfiguration {
|
|
128
156
|
[PREFERENCE_NAME_ENABLE_AI]: boolean | undefined;
|
|
129
157
|
[PREFERENCE_NAME_PROMPT_TEMPLATES]: string | undefined;
|
|
130
158
|
[PREFERENCE_NAME_REQUEST_SETTINGS]: Array<RequestSetting> | undefined;
|
|
159
|
+
[PREFERENCE_NAME_MAX_RETRIES]: number | undefined;
|
|
160
|
+
[PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE]: NotificationType | undefined;
|
|
131
161
|
}
|
|
132
162
|
|
|
133
163
|
export interface RequestSetting {
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource.
|
|
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 { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
|
|
18
|
+
let disableJSDOM = enableJSDOM();
|
|
19
|
+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
|
20
|
+
FrontendApplicationConfigProvider.set({});
|
|
21
|
+
|
|
22
|
+
import { expect } from 'chai';
|
|
23
|
+
import {
|
|
24
|
+
LanguageModelRequest,
|
|
25
|
+
ToolRequest
|
|
26
|
+
} from '../common';
|
|
27
|
+
|
|
28
|
+
disableJSDOM();
|
|
29
|
+
|
|
30
|
+
interface ErrorObject {
|
|
31
|
+
error: boolean;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isErrorObject(obj: unknown): obj is ErrorObject {
|
|
36
|
+
return !!obj && typeof obj === 'object' && 'error' in obj && 'message' in obj;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// This class provides a minimal implementation focused solely on testing the toolCall method.
|
|
40
|
+
// We cannot extend FrontendLanguageModelRegistryImpl directly due to issues in the test environment:
|
|
41
|
+
// - FrontendLanguageModelRegistryImpl imports dependencies that transitively depend on 'p-queue'
|
|
42
|
+
// - p-queue is an ESM-only module that cannot be loaded in the current test environment
|
|
43
|
+
class TestableLanguageModelRegistry {
|
|
44
|
+
private requests = new Map<string, LanguageModelRequest>();
|
|
45
|
+
|
|
46
|
+
async toolCall(id: string, toolId: string, arg_string: string): Promise<unknown> {
|
|
47
|
+
if (!this.requests.has(id)) {
|
|
48
|
+
return { error: true, message: `No request found for ID '${id}'. The request may have been cancelled or completed.` };
|
|
49
|
+
}
|
|
50
|
+
const request = this.requests.get(id)!;
|
|
51
|
+
const tool = request.tools?.find(t => t.id === toolId);
|
|
52
|
+
if (tool) {
|
|
53
|
+
try {
|
|
54
|
+
return await tool.handler(arg_string);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
57
|
+
return { error: true, message: `Error executing tool '${toolId}': ${errorMessage}` };
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return { error: true, message: `Tool '${toolId}' not found in the available tools for this request.` };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Test helper method
|
|
64
|
+
setRequest(id: string, request: LanguageModelRequest): void {
|
|
65
|
+
this.requests.set(id, request);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('FrontendLanguageModelRegistryImpl toolCall functionality', () => {
|
|
70
|
+
let registry: TestableLanguageModelRegistry;
|
|
71
|
+
|
|
72
|
+
before(() => {
|
|
73
|
+
disableJSDOM = enableJSDOM();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
after(() => {
|
|
77
|
+
disableJSDOM();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
registry = new TestableLanguageModelRegistry();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('toolCall', () => {
|
|
85
|
+
it('should return error object when request ID does not exist', async () => {
|
|
86
|
+
const result = await registry.toolCall('nonexistent-id', 'test-tool', '{}');
|
|
87
|
+
|
|
88
|
+
expect(result).to.be.an('object');
|
|
89
|
+
expect(isErrorObject(result)).to.be.true;
|
|
90
|
+
if (isErrorObject(result)) {
|
|
91
|
+
expect(result.error).to.be.true;
|
|
92
|
+
expect(result.message).to.include('No request found for ID \'nonexistent-id\'');
|
|
93
|
+
expect(result.message).to.include('The request may have been cancelled or completed');
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should return error object when tool is not found', async () => {
|
|
98
|
+
// Set up a request without the requested tool
|
|
99
|
+
const requestId = 'test-request-id';
|
|
100
|
+
const mockRequest: LanguageModelRequest = {
|
|
101
|
+
messages: [],
|
|
102
|
+
tools: [
|
|
103
|
+
{
|
|
104
|
+
id: 'different-tool',
|
|
105
|
+
name: 'Different Tool',
|
|
106
|
+
description: 'A different tool',
|
|
107
|
+
parameters: {
|
|
108
|
+
type: 'object',
|
|
109
|
+
properties: {}
|
|
110
|
+
},
|
|
111
|
+
handler: () => Promise.resolve('success')
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
registry.setRequest(requestId, mockRequest);
|
|
117
|
+
|
|
118
|
+
const result = await registry.toolCall(requestId, 'nonexistent-tool', '{}');
|
|
119
|
+
|
|
120
|
+
expect(result).to.be.an('object');
|
|
121
|
+
expect(isErrorObject(result)).to.be.true;
|
|
122
|
+
if (isErrorObject(result)) {
|
|
123
|
+
expect(result.error).to.be.true;
|
|
124
|
+
expect(result.message).to.include('Tool \'nonexistent-tool\' not found in the available tools for this request');
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should call tool handler successfully when tool exists', async () => {
|
|
129
|
+
const requestId = 'test-request-id';
|
|
130
|
+
const toolId = 'test-tool';
|
|
131
|
+
const expectedResult = 'tool execution result';
|
|
132
|
+
|
|
133
|
+
const mockTool: ToolRequest = {
|
|
134
|
+
id: toolId,
|
|
135
|
+
name: 'Test Tool',
|
|
136
|
+
description: 'A test tool',
|
|
137
|
+
parameters: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {}
|
|
140
|
+
},
|
|
141
|
+
handler: (args: string) => Promise.resolve(expectedResult)
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const mockRequest: LanguageModelRequest = {
|
|
145
|
+
messages: [],
|
|
146
|
+
tools: [mockTool]
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
registry.setRequest(requestId, mockRequest);
|
|
150
|
+
|
|
151
|
+
const result = await registry.toolCall(requestId, toolId, '{}');
|
|
152
|
+
|
|
153
|
+
expect(result).to.equal(expectedResult);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle synchronous tool handler errors gracefully', async () => {
|
|
157
|
+
const requestId = 'test-request-id';
|
|
158
|
+
const toolId = 'error-tool';
|
|
159
|
+
const errorMessage = 'Tool execution failed';
|
|
160
|
+
|
|
161
|
+
const mockTool: ToolRequest = {
|
|
162
|
+
id: toolId,
|
|
163
|
+
name: 'Error Tool',
|
|
164
|
+
description: 'A tool that throws an error',
|
|
165
|
+
parameters: {
|
|
166
|
+
type: 'object',
|
|
167
|
+
properties: {}
|
|
168
|
+
},
|
|
169
|
+
handler: () => {
|
|
170
|
+
throw new Error(errorMessage);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const mockRequest: LanguageModelRequest = {
|
|
175
|
+
messages: [],
|
|
176
|
+
tools: [mockTool]
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
registry.setRequest(requestId, mockRequest);
|
|
180
|
+
|
|
181
|
+
const result = await registry.toolCall(requestId, toolId, '{}');
|
|
182
|
+
|
|
183
|
+
expect(result).to.be.an('object');
|
|
184
|
+
expect(isErrorObject(result)).to.be.true;
|
|
185
|
+
if (isErrorObject(result)) {
|
|
186
|
+
expect(result.error).to.be.true;
|
|
187
|
+
expect(result.message).to.include(`Error executing tool '${toolId}': ${errorMessage}`);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle non-Error exceptions gracefully', async () => {
|
|
192
|
+
const requestId = 'test-request-id';
|
|
193
|
+
const toolId = 'string-error-tool';
|
|
194
|
+
const errorMessage = 'String error';
|
|
195
|
+
|
|
196
|
+
const mockTool: ToolRequest = {
|
|
197
|
+
id: toolId,
|
|
198
|
+
name: 'String Error Tool',
|
|
199
|
+
description: 'A tool that throws a string',
|
|
200
|
+
parameters: {
|
|
201
|
+
type: 'object',
|
|
202
|
+
properties: {}
|
|
203
|
+
},
|
|
204
|
+
handler: () => {
|
|
205
|
+
// eslint-disable-next-line no-throw-literal
|
|
206
|
+
throw errorMessage;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const mockRequest: LanguageModelRequest = {
|
|
211
|
+
messages: [],
|
|
212
|
+
tools: [mockTool]
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
registry.setRequest(requestId, mockRequest);
|
|
216
|
+
|
|
217
|
+
const result = await registry.toolCall(requestId, toolId, '{}');
|
|
218
|
+
|
|
219
|
+
expect(result).to.be.an('object');
|
|
220
|
+
expect(isErrorObject(result)).to.be.true;
|
|
221
|
+
if (isErrorObject(result)) {
|
|
222
|
+
expect(result.error).to.be.true;
|
|
223
|
+
expect(result.message).to.include(`Error executing tool '${toolId}': ${errorMessage}`);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle asynchronous tool handler errors gracefully', async () => {
|
|
228
|
+
const requestId = 'test-request-id';
|
|
229
|
+
const toolId = 'async-error-tool';
|
|
230
|
+
const errorMessage = 'Async tool execution failed';
|
|
231
|
+
|
|
232
|
+
const mockTool: ToolRequest = {
|
|
233
|
+
id: toolId,
|
|
234
|
+
name: 'Async Error Tool',
|
|
235
|
+
description: 'A tool that returns a rejected promise',
|
|
236
|
+
parameters: {
|
|
237
|
+
type: 'object',
|
|
238
|
+
properties: {}
|
|
239
|
+
},
|
|
240
|
+
handler: () => Promise.reject(new Error(errorMessage))
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const mockRequest: LanguageModelRequest = {
|
|
244
|
+
messages: [],
|
|
245
|
+
tools: [mockTool]
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
registry.setRequest(requestId, mockRequest);
|
|
249
|
+
|
|
250
|
+
const result = await registry.toolCall(requestId, toolId, '{}');
|
|
251
|
+
|
|
252
|
+
expect(result).to.be.an('object');
|
|
253
|
+
expect(isErrorObject(result)).to.be.true;
|
|
254
|
+
if (isErrorObject(result)) {
|
|
255
|
+
expect(result.error).to.be.true;
|
|
256
|
+
expect(result.message).to.include(`Error executing tool '${toolId}': ${errorMessage}`);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle tool handler with no tools array', async () => {
|
|
261
|
+
const requestId = 'test-request-id';
|
|
262
|
+
const mockRequest: LanguageModelRequest = {
|
|
263
|
+
messages: []
|
|
264
|
+
// No tools property
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
registry.setRequest(requestId, mockRequest);
|
|
268
|
+
|
|
269
|
+
const result = await registry.toolCall(requestId, 'any-tool', '{}');
|
|
270
|
+
|
|
271
|
+
expect(result).to.be.an('object');
|
|
272
|
+
expect(isErrorObject(result)).to.be.true;
|
|
273
|
+
if (isErrorObject(result)) {
|
|
274
|
+
expect(result.error).to.be.true;
|
|
275
|
+
expect(result.message).to.include('Tool \'any-tool\' not found in the available tools for this request');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should handle tool handler with empty tools array', async () => {
|
|
280
|
+
const requestId = 'test-request-id';
|
|
281
|
+
const mockRequest: LanguageModelRequest = {
|
|
282
|
+
messages: [],
|
|
283
|
+
tools: []
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
registry.setRequest(requestId, mockRequest);
|
|
287
|
+
|
|
288
|
+
const result = await registry.toolCall(requestId, 'any-tool', '{}');
|
|
289
|
+
|
|
290
|
+
expect(result).to.be.an('object');
|
|
291
|
+
expect(isErrorObject(result)).to.be.true;
|
|
292
|
+
if (isErrorObject(result)) {
|
|
293
|
+
expect(result.error).to.be.true;
|
|
294
|
+
expect(result.message).to.include('Tool \'any-tool\' not found in the available tools for this request');
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// *****************************************************************************
|
|
2
|
-
// Copyright (C)
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH.
|
|
3
3
|
//
|
|
4
4
|
// This program and the accompanying materials are made available under the
|
|
5
5
|
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
LanguageModelResponse,
|
|
44
44
|
LanguageModelSelector,
|
|
45
45
|
LanguageModelStreamResponsePart,
|
|
46
|
+
ToolCallResult,
|
|
46
47
|
} from '../common';
|
|
47
48
|
|
|
48
49
|
@injectable()
|
|
@@ -58,7 +59,7 @@ export class LanguageModelDelegateClientImpl
|
|
|
58
59
|
this.receiver.send(id, token);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
toolCall(requestId: string, toolId: string, args_string: string): Promise<
|
|
62
|
+
toolCall(requestId: string, toolId: string, args_string: string): Promise<ToolCallResult> {
|
|
62
63
|
return this.receiver.toolCall(requestId, toolId, args_string);
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -236,8 +237,8 @@ export class FrontendLanguageModelRegistryImpl
|
|
|
236
237
|
};
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
protected streams = new Map<string, StreamState>();
|
|
241
|
+
protected requests = new Map<string, LanguageModelRequest>();
|
|
241
242
|
|
|
242
243
|
async *getIterable(
|
|
243
244
|
state: StreamState
|
|
@@ -281,16 +282,21 @@ export class FrontendLanguageModelRegistryImpl
|
|
|
281
282
|
}
|
|
282
283
|
|
|
283
284
|
// called by backend once tool is invoked
|
|
284
|
-
toolCall(id: string, toolId: string, arg_string: string): Promise<
|
|
285
|
+
async toolCall(id: string, toolId: string, arg_string: string): Promise<ToolCallResult> {
|
|
285
286
|
if (!this.requests.has(id)) {
|
|
286
|
-
|
|
287
|
+
return { error: true, message: `No request found for ID '${id}'. The request may have been cancelled or completed.` };
|
|
287
288
|
}
|
|
288
289
|
const request = this.requests.get(id)!;
|
|
289
290
|
const tool = request.tools?.find(t => t.id === toolId);
|
|
290
291
|
if (tool) {
|
|
291
|
-
|
|
292
|
+
try {
|
|
293
|
+
return await tool.handler(arg_string);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
296
|
+
return { error: true, message: `Error executing tool '${toolId}': ${errorMessage}` };
|
|
297
|
+
};
|
|
292
298
|
}
|
|
293
|
-
|
|
299
|
+
return { error: true, message: `Tool '${toolId}' not found in the available tools for this request.` };
|
|
294
300
|
}
|
|
295
301
|
|
|
296
302
|
// called by backend via the "delegate client" with the error to use for rejection
|
|
@@ -37,6 +37,13 @@ export interface AIVariableDropResult {
|
|
|
37
37
|
text?: string
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
export type AIVariablePasteHandler = (event: ClipboardEvent, context: AIVariableContext) => Promise<AIVariablePasteResult | undefined>;
|
|
41
|
+
|
|
42
|
+
export interface AIVariablePasteResult {
|
|
43
|
+
variables: AIVariableResolutionRequest[],
|
|
44
|
+
text?: string
|
|
45
|
+
};
|
|
46
|
+
|
|
40
47
|
export interface AIVariableCompletionContext {
|
|
41
48
|
/** Portion of user input to be used for filtering completion candidates. */
|
|
42
49
|
userInput: string;
|
|
@@ -79,6 +86,10 @@ export interface FrontendVariableService extends AIVariableService {
|
|
|
79
86
|
unregisterDropHandler(handler: AIVariableDropHandler): void;
|
|
80
87
|
getDropResult(event: DragEvent, context: AIVariableContext): Promise<AIVariableDropResult>;
|
|
81
88
|
|
|
89
|
+
registerPasteHandler(handler: AIVariablePasteHandler): Disposable;
|
|
90
|
+
unregisterPasteHandler(handler: AIVariablePasteHandler): void;
|
|
91
|
+
getPasteResult(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult>;
|
|
92
|
+
|
|
82
93
|
registerOpener(variable: AIVariable, opener: AIVariableOpener): Disposable;
|
|
83
94
|
unregisterOpener(variable: AIVariable, opener: AIVariableOpener): void;
|
|
84
95
|
getOpener(name: string, arg: string | undefined, context: AIVariableContext): Promise<AIVariableOpener | undefined>;
|
|
@@ -92,6 +103,7 @@ export interface FrontendVariableContribution {
|
|
|
92
103
|
@injectable()
|
|
93
104
|
export class DefaultFrontendVariableService extends DefaultAIVariableService implements FrontendApplicationContribution, FrontendVariableService {
|
|
94
105
|
protected dropHandlers = new Set<AIVariableDropHandler>();
|
|
106
|
+
protected pasteHandlers = new Set<AIVariablePasteHandler>();
|
|
95
107
|
|
|
96
108
|
@inject(MessageService) protected readonly messageService: MessageService;
|
|
97
109
|
@inject(AIVariableResourceResolver) protected readonly aiResourceResolver: AIVariableResourceResolver;
|
|
@@ -125,6 +137,30 @@ export class DefaultFrontendVariableService extends DefaultAIVariableService imp
|
|
|
125
137
|
return { variables, text };
|
|
126
138
|
}
|
|
127
139
|
|
|
140
|
+
registerPasteHandler(handler: AIVariablePasteHandler): Disposable {
|
|
141
|
+
this.pasteHandlers.add(handler);
|
|
142
|
+
return Disposable.create(() => this.unregisterPasteHandler(handler));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
unregisterPasteHandler(handler: AIVariablePasteHandler): void {
|
|
146
|
+
this.pasteHandlers.delete(handler);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async getPasteResult(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult> {
|
|
150
|
+
let text: string | undefined = undefined;
|
|
151
|
+
const variables: AIVariableResolutionRequest[] = [];
|
|
152
|
+
for (const handler of this.pasteHandlers) {
|
|
153
|
+
const result = await handler(event, context);
|
|
154
|
+
if (result) {
|
|
155
|
+
variables.push(...result.variables);
|
|
156
|
+
if (text === undefined) {
|
|
157
|
+
text = result.text;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { variables, text };
|
|
162
|
+
}
|
|
163
|
+
|
|
128
164
|
registerOpener(variable: AIVariable, opener: AIVariableOpener): Disposable {
|
|
129
165
|
const key = this.getKey(variable.name);
|
|
130
166
|
if (!this.variables.get(key)) {
|
package/src/browser/index.ts
CHANGED
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
+
export * from './agent-completion-notification-service';
|
|
18
|
+
export * from './os-notification-service';
|
|
19
|
+
export * from './window-blink-service';
|
|
17
20
|
export * from './ai-activation-service';
|
|
21
|
+
export * from './ai-command-handler-factory';
|
|
18
22
|
export * from './ai-core-frontend-application-contribution';
|
|
19
23
|
export * from './ai-core-frontend-module';
|
|
20
24
|
export * from './ai-core-preferences';
|
|
@@ -24,6 +28,7 @@ export * from './frontend-language-model-registry';
|
|
|
24
28
|
export * from './frontend-variable-service';
|
|
25
29
|
export * from './prompttemplate-contribution';
|
|
26
30
|
export * from './theia-variable-contribution';
|
|
31
|
+
export * from './open-editors-variable-contribution';
|
|
27
32
|
export * from './frontend-variable-service';
|
|
28
33
|
export * from './ai-core-command-contribution';
|
|
29
34
|
export * from '../common/language-model-service';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 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 { MaybePromise, nls } from '@theia/core';
|
|
18
|
+
import { injectable, inject } from '@theia/core/shared/inversify';
|
|
19
|
+
import { EditorManager } from '@theia/editor/lib/browser';
|
|
20
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
21
|
+
import URI from '@theia/core/lib/common/uri';
|
|
22
|
+
import { AIVariable, ResolvedAIVariable, AIVariableContribution, AIVariableResolver, AIVariableService, AIVariableResolutionRequest, AIVariableContext } from '../common';
|
|
23
|
+
|
|
24
|
+
export const OPEN_EDITORS_VARIABLE: AIVariable = {
|
|
25
|
+
id: 'openEditors',
|
|
26
|
+
description: nls.localize('theia/ai/core/openEditorsVariable/description', 'A comma-separated list of all currently open files, relative to the workspace root.'),
|
|
27
|
+
name: 'openEditors',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const OPEN_EDITORS_SHORT_VARIABLE: AIVariable = {
|
|
31
|
+
id: 'openEditorsShort',
|
|
32
|
+
description: nls.localize('theia/ai/core/openEditorsShortVariable/description', 'Short reference to all currently open files (relative paths, comma-separated)'),
|
|
33
|
+
name: '_ff',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
@injectable()
|
|
37
|
+
export class OpenEditorsVariableContribution implements AIVariableContribution, AIVariableResolver {
|
|
38
|
+
|
|
39
|
+
@inject(EditorManager)
|
|
40
|
+
protected readonly editorManager: EditorManager;
|
|
41
|
+
|
|
42
|
+
@inject(WorkspaceService)
|
|
43
|
+
protected readonly workspaceService: WorkspaceService;
|
|
44
|
+
|
|
45
|
+
registerVariables(service: AIVariableService): void {
|
|
46
|
+
service.registerResolver(OPEN_EDITORS_VARIABLE, this);
|
|
47
|
+
service.registerResolver(OPEN_EDITORS_SHORT_VARIABLE, this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
canResolve(request: AIVariableResolutionRequest, _context: AIVariableContext): MaybePromise<number> {
|
|
51
|
+
return (request.variable.name === OPEN_EDITORS_VARIABLE.name || request.variable.name === OPEN_EDITORS_SHORT_VARIABLE.name) ? 50 : 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async resolve(request: AIVariableResolutionRequest, _context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
|
|
55
|
+
if (request.variable.name !== OPEN_EDITORS_VARIABLE.name && request.variable.name !== OPEN_EDITORS_SHORT_VARIABLE.name) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const openFiles = this.getAllOpenFilesRelative();
|
|
60
|
+
return {
|
|
61
|
+
variable: request.variable,
|
|
62
|
+
value: openFiles
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
protected getAllOpenFilesRelative(): string {
|
|
67
|
+
const openFiles: string[] = [];
|
|
68
|
+
|
|
69
|
+
// Get all open editors from the editor manager
|
|
70
|
+
for (const editor of this.editorManager.all) {
|
|
71
|
+
const uri = editor.getResourceUri();
|
|
72
|
+
if (uri) {
|
|
73
|
+
const relativePath = this.getWorkspaceRelativePath(uri);
|
|
74
|
+
if (relativePath) {
|
|
75
|
+
openFiles.push(`'${relativePath}'`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return openFiles.join(', ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected getWorkspaceRelativePath(uri: URI): string | undefined {
|
|
84
|
+
const workspaceRootUri = this.workspaceService.getWorkspaceRootUri(uri);
|
|
85
|
+
const path = workspaceRootUri && workspaceRootUri.path.relative(uri.path);
|
|
86
|
+
return path && path.toString();
|
|
87
|
+
}
|
|
88
|
+
}
|