@theia/ai-core 1.63.0-next.52 → 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 +7 -1
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-core-preferences.d.ts +3 -0
- package/lib/browser/ai-core-preferences.d.ts.map +1 -1
- package/lib/browser/ai-core-preferences.js +14 -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/index.d.ts +4 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +4 -0
- package/lib/browser/index.js.map +1 -1
- 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 +24 -3
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +4 -0
- 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/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 +8 -0
- package/src/browser/ai-core-preferences.ts +20 -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/index.ts +4 -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 +12 -3
- 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
|
@@ -0,0 +1,242 @@
|
|
|
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 { injectable, inject } from '@theia/core/shared/inversify';
|
|
18
|
+
import { PreferenceService } from '@theia/core/lib/browser/preferences';
|
|
19
|
+
import { MessageService } from '@theia/core/lib/common/message-service';
|
|
20
|
+
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
|
|
21
|
+
import { nls } from '@theia/core/lib/common/nls';
|
|
22
|
+
import {
|
|
23
|
+
PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE,
|
|
24
|
+
} from './ai-core-preferences';
|
|
25
|
+
import { AgentService } from '../common/agent-service';
|
|
26
|
+
import { AISettingsService } from '../common/settings-service';
|
|
27
|
+
import { OSNotificationService } from './os-notification-service';
|
|
28
|
+
import { WindowBlinkService } from './window-blink-service';
|
|
29
|
+
import {
|
|
30
|
+
NotificationType,
|
|
31
|
+
NOTIFICATION_TYPE_OFF,
|
|
32
|
+
NOTIFICATION_TYPE_OS_NOTIFICATION,
|
|
33
|
+
NOTIFICATION_TYPE_MESSAGE,
|
|
34
|
+
NOTIFICATION_TYPE_BLINK,
|
|
35
|
+
} from '../common/notification-types';
|
|
36
|
+
|
|
37
|
+
@injectable()
|
|
38
|
+
export class AgentCompletionNotificationService {
|
|
39
|
+
@inject(PreferenceService)
|
|
40
|
+
protected readonly preferenceService: PreferenceService;
|
|
41
|
+
|
|
42
|
+
@inject(AgentService)
|
|
43
|
+
protected readonly agentService: AgentService;
|
|
44
|
+
|
|
45
|
+
@inject(AISettingsService)
|
|
46
|
+
protected readonly settingsService: AISettingsService;
|
|
47
|
+
|
|
48
|
+
@inject(OSNotificationService)
|
|
49
|
+
protected readonly osNotificationService: OSNotificationService;
|
|
50
|
+
|
|
51
|
+
@inject(MessageService)
|
|
52
|
+
protected readonly messageService: MessageService;
|
|
53
|
+
|
|
54
|
+
@inject(WindowBlinkService)
|
|
55
|
+
protected readonly windowBlinkService: WindowBlinkService;
|
|
56
|
+
|
|
57
|
+
@inject(ApplicationShell)
|
|
58
|
+
protected readonly shell: ApplicationShell;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Show a completion notification for the specified agent if enabled in preferences.
|
|
62
|
+
*
|
|
63
|
+
* @param agentId The unique identifier of the agent
|
|
64
|
+
* @param taskDescription Optional description of the completed task
|
|
65
|
+
*/
|
|
66
|
+
async showCompletionNotification(
|
|
67
|
+
agentId: string,
|
|
68
|
+
taskDescription?: string,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const notificationType =
|
|
71
|
+
await this.getNotificationTypeForAgent(agentId);
|
|
72
|
+
|
|
73
|
+
if (notificationType === NOTIFICATION_TYPE_OFF || this.isChatWidgetFocused()) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const agentName = this.resolveAgentName(agentId);
|
|
79
|
+
await this.executeNotificationType(
|
|
80
|
+
agentName,
|
|
81
|
+
taskDescription,
|
|
82
|
+
notificationType,
|
|
83
|
+
);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(
|
|
86
|
+
'Failed to show agent completion notification:',
|
|
87
|
+
error,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the display name for an agent by its ID.
|
|
94
|
+
*
|
|
95
|
+
* @param agentId The unique identifier of the agent
|
|
96
|
+
* @returns The agent's display name or the agent ID if not found
|
|
97
|
+
*/
|
|
98
|
+
protected resolveAgentName(agentId: string): string {
|
|
99
|
+
try {
|
|
100
|
+
const agents = this.agentService.getAllAgents();
|
|
101
|
+
const agent = agents.find(a => a.id === agentId);
|
|
102
|
+
return agent?.name || agentId;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.warn(
|
|
105
|
+
`Failed to resolve agent name for ID '${agentId}':`,
|
|
106
|
+
error,
|
|
107
|
+
);
|
|
108
|
+
return agentId;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the preferred notification type for a specific agent.
|
|
114
|
+
* If no agent-specific preference is set, returns the global default notification type.
|
|
115
|
+
*/
|
|
116
|
+
protected async getNotificationTypeForAgent(
|
|
117
|
+
agentId: string,
|
|
118
|
+
): Promise<NotificationType> {
|
|
119
|
+
const agentSettings =
|
|
120
|
+
await this.settingsService.getAgentSettings(agentId);
|
|
121
|
+
const agentNotificationType = agentSettings?.completionNotification as NotificationType;
|
|
122
|
+
|
|
123
|
+
// If agent has no specific setting, use the global default
|
|
124
|
+
if (!agentNotificationType) {
|
|
125
|
+
return this.preferenceService.get<NotificationType>(
|
|
126
|
+
PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE,
|
|
127
|
+
NOTIFICATION_TYPE_OFF,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return agentNotificationType;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Execute the specified notification type.
|
|
136
|
+
*/
|
|
137
|
+
private async executeNotificationType(
|
|
138
|
+
agentName: string,
|
|
139
|
+
taskDescription: string | undefined,
|
|
140
|
+
type: NotificationType,
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
switch (type) {
|
|
143
|
+
case NOTIFICATION_TYPE_OS_NOTIFICATION:
|
|
144
|
+
await this.showOSNotification(agentName, taskDescription);
|
|
145
|
+
break;
|
|
146
|
+
case NOTIFICATION_TYPE_MESSAGE:
|
|
147
|
+
await this.showMessageServiceNotification(
|
|
148
|
+
agentName,
|
|
149
|
+
taskDescription,
|
|
150
|
+
);
|
|
151
|
+
break;
|
|
152
|
+
case NOTIFICATION_TYPE_BLINK:
|
|
153
|
+
await this.showBlinkNotification(agentName);
|
|
154
|
+
break;
|
|
155
|
+
default:
|
|
156
|
+
throw new Error(`Unknown notification type: ${type}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Show OS notification directly.
|
|
162
|
+
*/
|
|
163
|
+
protected async showOSNotification(
|
|
164
|
+
agentName: string,
|
|
165
|
+
taskDescription?: string,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
const result =
|
|
168
|
+
await this.osNotificationService.showAgentCompletionNotification(
|
|
169
|
+
agentName,
|
|
170
|
+
taskDescription,
|
|
171
|
+
);
|
|
172
|
+
if (!result.success) {
|
|
173
|
+
throw new Error(`OS notification failed: ${result.error}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Show MessageService notification.
|
|
179
|
+
*/
|
|
180
|
+
protected async showMessageServiceNotification(
|
|
181
|
+
agentName: string,
|
|
182
|
+
taskDescription?: string,
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const message = taskDescription
|
|
185
|
+
? nls.localize(
|
|
186
|
+
'theia/ai-core/agentCompletionWithTask',
|
|
187
|
+
'Agent "{0}" has completed the task: {1}',
|
|
188
|
+
agentName,
|
|
189
|
+
taskDescription,
|
|
190
|
+
)
|
|
191
|
+
: nls.localize(
|
|
192
|
+
'theia/ai-core/agentCompletionMessage',
|
|
193
|
+
'Agent "{0}" has completed its task.',
|
|
194
|
+
agentName,
|
|
195
|
+
);
|
|
196
|
+
this.messageService.info(message);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Show window blink notification.
|
|
201
|
+
*/
|
|
202
|
+
protected async showBlinkNotification(agentName: string): Promise<void> {
|
|
203
|
+
const result = await this.windowBlinkService.blinkWindow(agentName);
|
|
204
|
+
if (!result.success) {
|
|
205
|
+
throw new Error(
|
|
206
|
+
`Window blink notification failed: ${result.error}`,
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if OS notifications are supported and enabled.
|
|
213
|
+
*/
|
|
214
|
+
isOSNotificationSupported(): boolean {
|
|
215
|
+
return this.osNotificationService.isNotificationSupported();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get the current OS notification permission status.
|
|
220
|
+
*/
|
|
221
|
+
getOSNotificationPermission(): NotificationPermission {
|
|
222
|
+
return this.osNotificationService.getPermissionStatus();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Request OS notification permission from the user.
|
|
227
|
+
*/
|
|
228
|
+
async requestOSNotificationPermission(): Promise<NotificationPermission> {
|
|
229
|
+
return this.osNotificationService.requestPermission();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if any chat widget currently has focus.
|
|
234
|
+
*/
|
|
235
|
+
protected isChatWidgetFocused(): boolean {
|
|
236
|
+
const activeWidget = this.shell.activeWidget;
|
|
237
|
+
if (!activeWidget) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
return activeWidget.id === 'chat-view-widget';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
|
|
17
17
|
import { nls } from '@theia/core';
|
|
18
18
|
import { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution';
|
|
19
|
+
import {
|
|
20
|
+
NOTIFICATION_TYPES
|
|
21
|
+
} from '../common/notification-types';
|
|
19
22
|
|
|
20
23
|
export const AGENT_SETTINGS_PREF = 'ai-features.agentSettings';
|
|
21
24
|
|
|
@@ -65,6 +68,17 @@ export const AgentSettingsPreferenceSchema: PreferenceSchema = {
|
|
|
65
68
|
additionalProperties: {
|
|
66
69
|
type: 'string'
|
|
67
70
|
}
|
|
71
|
+
},
|
|
72
|
+
completionNotification: {
|
|
73
|
+
type: 'string',
|
|
74
|
+
enum: [...NOTIFICATION_TYPES],
|
|
75
|
+
title: nls.localize('theia/ai/agents/completionNotification/title', 'Completion Notification'),
|
|
76
|
+
markdownDescription: nls.localize('theia/ai/agents/completionNotification/mdDescription',
|
|
77
|
+
'Notification behavior when this agent completes a task. If not set, the global default notification setting will be used.\n\
|
|
78
|
+
- `os-notification`: Show OS/system notifications\n\
|
|
79
|
+
- `message`: Show notifications in the status bar/message area\n\
|
|
80
|
+
- `blink`: Blink or highlight the UI\n\
|
|
81
|
+
- `off`: Disable notifications for this agent')
|
|
68
82
|
}
|
|
69
83
|
},
|
|
70
84
|
required: ['languageModelRequirements']
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
|
+
|
|
16
17
|
import { bindContributionProvider, CommandContribution, CommandHandler, ResourceResolver } from '@theia/core';
|
|
17
18
|
import {
|
|
18
19
|
RemoteConnectionProvider,
|
|
@@ -72,6 +73,9 @@ import { FrontendLanguageModelServiceImpl } from './frontend-language-model-serv
|
|
|
72
73
|
import { TokenUsageFrontendService } from './token-usage-frontend-service';
|
|
73
74
|
import { TokenUsageFrontendServiceImpl, TokenUsageServiceClientImpl } from './token-usage-frontend-service-impl';
|
|
74
75
|
import { AIVariableUriLabelProvider } from './ai-variable-uri-label-provider';
|
|
76
|
+
import { AgentCompletionNotificationService } from './agent-completion-notification-service';
|
|
77
|
+
import { OSNotificationService } from './os-notification-service';
|
|
78
|
+
import { WindowBlinkService } from './window-blink-service';
|
|
75
79
|
|
|
76
80
|
export default new ContainerModule(bind => {
|
|
77
81
|
bindContributionProvider(bind, LanguageModelProvider);
|
|
@@ -165,6 +169,10 @@ export default new ContainerModule(bind => {
|
|
|
165
169
|
bind(ResourceResolver).toService(AIVariableResourceResolver);
|
|
166
170
|
bind(AIVariableUriLabelProvider).toSelf().inSingletonScope();
|
|
167
171
|
bind(LabelProviderContribution).toService(AIVariableUriLabelProvider);
|
|
172
|
+
|
|
173
|
+
bind(AgentCompletionNotificationService).toSelf().inSingletonScope();
|
|
174
|
+
bind(OSNotificationService).toSelf().inSingletonScope();
|
|
175
|
+
bind(WindowBlinkService).toSelf().inSingletonScope();
|
|
168
176
|
bind(ConfigurableInMemoryResources).toSelf().inSingletonScope();
|
|
169
177
|
bind(ResourceResolver).toService(ConfigurableInMemoryResources);
|
|
170
178
|
});
|
|
@@ -18,12 +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';
|
|
26
31
|
export const PREFERENCE_NAME_MAX_RETRIES = 'ai-features.modelSettings.maxRetries';
|
|
32
|
+
export const PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE = 'ai-features.notifications.default';
|
|
27
33
|
|
|
28
34
|
export const aiCorePreferenceSchema: PreferenceSchema = {
|
|
29
35
|
type: 'object',
|
|
@@ -130,14 +136,28 @@ export const aiCorePreferenceSchema: PreferenceSchema = {
|
|
|
130
136
|
type: 'number',
|
|
131
137
|
minimum: 0,
|
|
132
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
|
|
133
151
|
}
|
|
134
152
|
}
|
|
135
153
|
};
|
|
154
|
+
|
|
136
155
|
export interface AICoreConfiguration {
|
|
137
156
|
[PREFERENCE_NAME_ENABLE_AI]: boolean | undefined;
|
|
138
157
|
[PREFERENCE_NAME_PROMPT_TEMPLATES]: string | undefined;
|
|
139
158
|
[PREFERENCE_NAME_REQUEST_SETTINGS]: Array<RequestSetting> | undefined;
|
|
140
159
|
[PREFERENCE_NAME_MAX_RETRIES]: number | undefined;
|
|
160
|
+
[PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE]: NotificationType | undefined;
|
|
141
161
|
}
|
|
142
162
|
|
|
143
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
|
+
});
|