@theia/ai-chat 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-delegation-tool.d.ts +25 -0
- package/lib/browser/agent-delegation-tool.d.ts.map +1 -0
- package/lib/browser/agent-delegation-tool.js +171 -0
- package/lib/browser/agent-delegation-tool.js.map +1 -0
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +8 -0
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-file-element.d.ts +47 -8
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +207 -31
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/chat-tool-preferences.d.ts +1 -1
- package/lib/browser/chat-tool-preferences.d.ts.map +1 -1
- package/lib/browser/chat-tool-preferences.js +4 -4
- package/lib/browser/chat-tool-preferences.js.map +1 -1
- package/lib/browser/chat-tool-request-service.js +1 -1
- package/lib/browser/chat-tool-request-service.js.map +1 -1
- package/lib/browser/delegation-response-content.d.ts +20 -0
- package/lib/browser/delegation-response-content.d.ts.map +1 -0
- package/lib/browser/delegation-response-content.js +51 -0
- package/lib/browser/delegation-response-content.js.map +1 -0
- package/lib/browser/file-chat-variable-contribution.d.ts +15 -1
- package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +111 -5
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/frontend-chat-service.d.ts +1 -1
- package/lib/browser/frontend-chat-service.d.ts.map +1 -1
- package/lib/browser/frontend-chat-service.js +2 -13
- package/lib/browser/frontend-chat-service.js.map +1 -1
- package/lib/browser/image-context-variable-contribution.d.ts +27 -0
- package/lib/browser/image-context-variable-contribution.d.ts.map +1 -0
- package/lib/browser/image-context-variable-contribution.js +149 -0
- package/lib/browser/image-context-variable-contribution.js.map +1 -0
- package/lib/browser/task-context-service.d.ts +9 -3
- package/lib/browser/task-context-service.d.ts.map +1 -1
- package/lib/browser/task-context-service.js +111 -9
- package/lib/browser/task-context-service.js.map +1 -1
- package/lib/browser/task-context-storage-service.d.ts +1 -0
- package/lib/browser/task-context-storage-service.d.ts.map +1 -1
- package/lib/browser/task-context-storage-service.js +4 -1
- package/lib/browser/task-context-storage-service.js.map +1 -1
- package/lib/common/change-set.js +1 -1
- package/lib/common/change-set.js.map +1 -1
- package/lib/common/chat-agent-service.d.ts +1 -0
- package/lib/common/chat-agent-service.d.ts.map +1 -1
- package/lib/common/chat-agent-service.js +2 -1
- package/lib/common/chat-agent-service.js.map +1 -1
- package/lib/common/chat-agents.d.ts +2 -2
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +21 -5
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-model.d.ts +7 -7
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +1 -1
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +3 -6
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-service.d.ts +14 -2
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +36 -10
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-naming-service.js +2 -2
- package/lib/common/chat-session-naming-service.js.map +1 -1
- package/lib/common/chat-session-summary-agent-prompt.js +3 -3
- package/lib/common/chat-session-summary-agent-prompt.js.map +1 -1
- package/lib/common/chat-tool-request-service.d.ts +2 -2
- package/lib/common/chat-tool-request-service.d.ts.map +1 -1
- package/lib/common/image-context-variable.d.ts +29 -0
- package/lib/common/image-context-variable.d.ts.map +1 -0
- package/lib/common/image-context-variable.js +99 -0
- package/lib/common/image-context-variable.js.map +1 -0
- package/package.json +11 -10
- package/src/browser/agent-delegation-tool.ts +207 -0
- package/src/browser/ai-chat-frontend-module.ts +20 -2
- package/src/browser/change-set-file-element.ts +236 -32
- package/src/browser/chat-tool-preferences.ts +4 -4
- package/src/browser/chat-tool-request-service.ts +1 -1
- package/src/browser/delegation-response-content.ts +55 -0
- package/src/browser/file-chat-variable-contribution.ts +120 -6
- package/src/browser/frontend-chat-service.ts +2 -11
- package/src/browser/image-context-variable-contribution.ts +153 -0
- package/src/browser/task-context-service.ts +115 -9
- package/src/browser/task-context-storage-service.ts +5 -1
- package/src/common/change-set.ts +1 -1
- package/src/common/chat-agent-service.ts +1 -0
- package/src/common/chat-agents.ts +26 -9
- package/src/common/chat-model.ts +16 -7
- package/src/common/chat-request-parser.ts +3 -12
- package/src/common/chat-service.ts +40 -10
- package/src/common/chat-session-naming-service.ts +2 -2
- package/src/common/chat-session-summary-agent-prompt.ts +3 -3
- package/src/common/chat-tool-request-service.ts +2 -2
- package/src/common/image-context-variable.ts +116 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 EclipseSource GmbH and others.
|
|
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 {
|
|
18
|
+
AIVariableContext, AIVariableContribution,
|
|
19
|
+
AIVariableOpener, AIVariableResolutionRequest, AIVariableResolver, ResolvedAIContextVariable
|
|
20
|
+
} from '@theia/ai-core';
|
|
21
|
+
import { FrontendVariableService, AIVariablePasteResult } from '@theia/ai-core/lib/browser';
|
|
22
|
+
import { Path, URI } from '@theia/core';
|
|
23
|
+
import { LabelProvider, LabelProviderContribution, open, OpenerService } from '@theia/core/lib/browser';
|
|
24
|
+
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
25
|
+
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
|
26
|
+
import { WorkspaceService } from '@theia/workspace/lib/browser';
|
|
27
|
+
import { IMAGE_CONTEXT_VARIABLE, ImageContextVariable, ImageContextVariableRequest } from '../common/image-context-variable';
|
|
28
|
+
|
|
29
|
+
@injectable()
|
|
30
|
+
export class ImageContextVariableContribution implements AIVariableContribution, AIVariableResolver, AIVariableOpener, LabelProviderContribution {
|
|
31
|
+
@inject(FileService)
|
|
32
|
+
protected readonly fileService: FileService;
|
|
33
|
+
|
|
34
|
+
@inject(WorkspaceService)
|
|
35
|
+
protected readonly wsService: WorkspaceService;
|
|
36
|
+
|
|
37
|
+
@inject(OpenerService)
|
|
38
|
+
protected readonly openerService: OpenerService;
|
|
39
|
+
|
|
40
|
+
@inject(LabelProvider)
|
|
41
|
+
protected readonly labelProvider: LabelProvider;
|
|
42
|
+
|
|
43
|
+
registerVariables(service: FrontendVariableService): void {
|
|
44
|
+
service.registerResolver(IMAGE_CONTEXT_VARIABLE, this);
|
|
45
|
+
service.registerOpener(IMAGE_CONTEXT_VARIABLE, this);
|
|
46
|
+
service.registerPasteHandler(this.handlePaste.bind(this));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async canResolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<number> {
|
|
50
|
+
return ImageContextVariable.isImageContextRequest(request) ? 1 : 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async resolve(request: AIVariableResolutionRequest, _: AIVariableContext): Promise<ResolvedAIContextVariable | undefined> {
|
|
54
|
+
return ImageContextVariable.resolve(request as ImageContextVariableRequest);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async canOpen(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<number> {
|
|
58
|
+
return ImageContextVariable.isImageContextRequest(request) && !!ImageContextVariable.parseRequest(request)?.wsRelativePath ? 1 : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async open(request: ImageContextVariableRequest, context: AIVariableContext): Promise<void> {
|
|
62
|
+
const uri = await this.toUri(request);
|
|
63
|
+
if (!uri) {
|
|
64
|
+
throw new Error('Unable to resolve URI for request.');
|
|
65
|
+
}
|
|
66
|
+
await open(this.openerService, uri);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
protected async toUri(request: ImageContextVariableRequest): Promise<URI | undefined> {
|
|
70
|
+
const variable = ImageContextVariable.parseRequest(request);
|
|
71
|
+
return variable?.wsRelativePath ? this.makeAbsolute(variable.wsRelativePath) : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async handlePaste(event: ClipboardEvent, context: AIVariableContext): Promise<AIVariablePasteResult | undefined> {
|
|
75
|
+
if (!event.clipboardData?.items) { return undefined; }
|
|
76
|
+
|
|
77
|
+
const variables: AIVariableResolutionRequest[] = [];
|
|
78
|
+
|
|
79
|
+
for (const item of event.clipboardData.items) {
|
|
80
|
+
if (item.type.startsWith('image/')) {
|
|
81
|
+
const blob = item.getAsFile();
|
|
82
|
+
if (blob) {
|
|
83
|
+
try {
|
|
84
|
+
const dataUrl = await this.readFileAsDataURL(blob);
|
|
85
|
+
// Extract the base64 data by removing the data URL prefix
|
|
86
|
+
// Format is like: data:image/png;base64,BASE64DATA
|
|
87
|
+
const imageData = dataUrl.substring(dataUrl.indexOf(',') + 1);
|
|
88
|
+
variables.push(ImageContextVariable.createRequest({
|
|
89
|
+
data: imageData,
|
|
90
|
+
name: blob.name || `pasted-image-${Date.now()}.png`,
|
|
91
|
+
mimeType: blob.type
|
|
92
|
+
}));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to process pasted image:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return variables.length > 0 ? { variables } : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private readFileAsDataURL(blob: Blob): Promise<string> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const reader = new FileReader();
|
|
106
|
+
reader.onload = e => {
|
|
107
|
+
if (!e.target?.result) {
|
|
108
|
+
reject(new Error('Failed to read file as data URL'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
resolve(e.target.result as string);
|
|
112
|
+
};
|
|
113
|
+
reader.onerror = () => reject(reader.error);
|
|
114
|
+
reader.readAsDataURL(blob);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
protected async makeAbsolute(pathStr: string): Promise<URI | undefined> {
|
|
119
|
+
const path = new Path(Path.normalizePathSeparator(pathStr));
|
|
120
|
+
if (!path.isAbsolute) {
|
|
121
|
+
const workspaceRoots = this.wsService.tryGetRoots();
|
|
122
|
+
const wsUris = workspaceRoots.map(root => root.resource.resolve(path));
|
|
123
|
+
for (const uri of wsUris) {
|
|
124
|
+
if (await this.fileService.exists(uri)) {
|
|
125
|
+
return uri;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const argUri = new URI(pathStr);
|
|
130
|
+
if (await this.fileService.exists(argUri)) {
|
|
131
|
+
return argUri;
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
canHandle(element: object): number {
|
|
137
|
+
return ImageContextVariable.isImageContextRequest(element) ? 10 : -1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getIcon(element: ImageContextVariableRequest): string | undefined {
|
|
141
|
+
const path = ImageContextVariable.parseArg(element.arg).wsRelativePath;
|
|
142
|
+
return path ? this.labelProvider.getIcon(new URI(path)) : undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getName(element: ImageContextVariableRequest): string | undefined {
|
|
146
|
+
return ImageContextVariable.parseArg(element.arg).name;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
getDetails(element: ImageContextVariableRequest): string | undefined {
|
|
150
|
+
const path = ImageContextVariable.parseArg(element.arg).wsRelativePath;
|
|
151
|
+
return path ? this.labelProvider.getDetails(new URI(path)) : '[pasted]';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
-
import { MaybePromise, ProgressService, URI, generateUuid, Event } from '@theia/core';
|
|
18
|
+
import { MaybePromise, ProgressService, URI, generateUuid, Event, EOL } from '@theia/core';
|
|
19
19
|
import { ChatAgent, ChatAgentLocation, ChatService, ChatSession, MutableChatModel, MutableChatRequestModel, ParsedChatRequestTextPart } from '../common';
|
|
20
|
+
import { PreferenceService } from '@theia/core/lib/browser';
|
|
20
21
|
import { ChatSessionSummaryAgent } from '../common/chat-session-summary-agent';
|
|
21
22
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
22
|
-
import { AgentService, PromptService } from '@theia/ai-core';
|
|
23
|
+
import { AgentService, PromptService, ResolvedPromptFragment } from '@theia/ai-core';
|
|
23
24
|
import { CHAT_SESSION_SUMMARY_PROMPT } from '../common/chat-session-summary-agent-prompt';
|
|
25
|
+
import { ChangeSetFileElementFactory } from './change-set-file-element';
|
|
26
|
+
import * as yaml from 'js-yaml';
|
|
24
27
|
|
|
25
28
|
export interface SummaryMetadata {
|
|
26
29
|
label: string;
|
|
@@ -33,7 +36,7 @@ export interface Summary extends SummaryMetadata {
|
|
|
33
36
|
id: string;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
|
-
export const TaskContextStorageService = Symbol('
|
|
39
|
+
export const TaskContextStorageService = Symbol('TaskContextStorageService');
|
|
37
40
|
export interface TaskContextStorageService {
|
|
38
41
|
onDidChange: Event<void>;
|
|
39
42
|
store(summary: Summary): MaybePromise<void>;
|
|
@@ -53,6 +56,9 @@ export class TaskContextService {
|
|
|
53
56
|
@inject(PromptService) protected readonly promptService: PromptService;
|
|
54
57
|
@inject(TaskContextStorageService) protected readonly storageService: TaskContextStorageService;
|
|
55
58
|
@inject(ProgressService) protected readonly progressService: ProgressService;
|
|
59
|
+
@inject(PreferenceService) protected readonly preferenceService: PreferenceService;
|
|
60
|
+
@inject(ChangeSetFileElementFactory)
|
|
61
|
+
protected readonly fileChangeFactory: ChangeSetFileElementFactory;
|
|
56
62
|
|
|
57
63
|
get onDidChange(): Event<void> {
|
|
58
64
|
return this.storageService.onDidChange;
|
|
@@ -77,18 +83,19 @@ export class TaskContextService {
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/** Returns an ID that can be used to refer to the summary in the future. */
|
|
80
|
-
async summarize(session: ChatSession, promptId?: string, agent?: ChatAgent): Promise<string> {
|
|
86
|
+
async summarize(session: ChatSession, promptId?: string, agent?: ChatAgent, override = true): Promise<string> {
|
|
81
87
|
const pending = this.pendingSummaries.get(session.id);
|
|
82
88
|
if (pending) { return pending.then(({ id }) => id); }
|
|
83
89
|
const existing = this.getSummaryForSession(session);
|
|
84
|
-
if (existing) { return existing.id; }
|
|
90
|
+
if (existing && !override) { return existing.id; }
|
|
85
91
|
const summaryId = generateUuid();
|
|
86
92
|
const summaryDeferred = new Deferred<Summary>();
|
|
87
93
|
const progress = await this.progressService.showProgress({ text: `Summarize: ${session.title || session.id}`, options: { location: 'ai-chat' } });
|
|
88
94
|
this.pendingSummaries.set(session.id, summaryDeferred.promise);
|
|
89
95
|
try {
|
|
96
|
+
const prompt = await this.getSystemPrompt(session, promptId);
|
|
90
97
|
const newSummary: Summary = {
|
|
91
|
-
summary: await this.getLlmSummary(session,
|
|
98
|
+
summary: await this.getLlmSummary(session, prompt, agent),
|
|
92
99
|
label: session.title || session.id,
|
|
93
100
|
sessionId: session.id,
|
|
94
101
|
id: summaryId
|
|
@@ -97,6 +104,13 @@ export class TaskContextService {
|
|
|
97
104
|
return summaryId;
|
|
98
105
|
} catch (err) {
|
|
99
106
|
summaryDeferred.reject(err);
|
|
107
|
+
const errorSummary: Summary = {
|
|
108
|
+
summary: `Summary creation failed: ${err instanceof Error ? err.message : typeof err === 'string' ? err : 'Unknown error'}`,
|
|
109
|
+
label: session.title || session.id,
|
|
110
|
+
sessionId: session.id,
|
|
111
|
+
id: summaryId
|
|
112
|
+
};
|
|
113
|
+
await this.storageService.store(errorSummary);
|
|
100
114
|
throw err;
|
|
101
115
|
} finally {
|
|
102
116
|
progress.cancel();
|
|
@@ -104,7 +118,95 @@ export class TaskContextService {
|
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
|
|
121
|
+
async update(session: ChatSession, promptId?: string, agent?: ChatAgent, override = true): Promise<string> {
|
|
122
|
+
// Get the existing summary for the session
|
|
123
|
+
const existingSummary = this.getSummaryForSession(session);
|
|
124
|
+
if (!existingSummary) {
|
|
125
|
+
// If no summary exists, create one instead
|
|
126
|
+
// TODO: Maybe we could also look into the task context folder and ask for the existing ones with an additional menu to create a new one?
|
|
127
|
+
return this.summarize(session, promptId, agent, override);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const progress = await this.progressService.showProgress({ text: `Updating: ${session.title || session.id}`, options: { location: 'ai-chat' } });
|
|
131
|
+
try {
|
|
132
|
+
const prompt = await this.getSystemPrompt(session, promptId);
|
|
133
|
+
if (!prompt) {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Get the task context file path
|
|
138
|
+
const taskContextStorageDirectory = this.preferenceService.get(
|
|
139
|
+
// preference key is defined in TASK_CONTEXT_STORAGE_DIRECTORY_PREF in @theia/ai-ide
|
|
140
|
+
'ai-features.promptTemplates.taskContextStorageDirectory',
|
|
141
|
+
'.prompts/task-contexts'
|
|
142
|
+
);
|
|
143
|
+
const taskContextFileVariable = session.model.context.getVariables().find(variableReq => variableReq.variable.id === 'file-provider' &&
|
|
144
|
+
typeof variableReq.arg === 'string' &&
|
|
145
|
+
(variableReq.arg.startsWith(taskContextStorageDirectory)));
|
|
146
|
+
|
|
147
|
+
// Check if we have a document path to update
|
|
148
|
+
if (taskContextFileVariable && typeof taskContextFileVariable.arg === 'string') {
|
|
149
|
+
// Set document path in prompt template
|
|
150
|
+
const documentPath = taskContextFileVariable.arg;
|
|
151
|
+
|
|
152
|
+
// Modify prompt to include the document path and content
|
|
153
|
+
prompt.text = prompt.text + '\nThe document to update is: ' + documentPath + '\n\n## Current Document Content\n\n' + existingSummary.summary;
|
|
154
|
+
|
|
155
|
+
// Get updated document content from LLM
|
|
156
|
+
const updatedDocumentContent = await this.getLlmSummary(session, prompt, agent);
|
|
157
|
+
|
|
158
|
+
if (existingSummary.uri) {
|
|
159
|
+
// updated document metadata shall be updated.
|
|
160
|
+
// otherwise, frontmatter won't be set
|
|
161
|
+
const frontmatter = {
|
|
162
|
+
sessionId: existingSummary.sessionId,
|
|
163
|
+
date: new Date().toISOString(),
|
|
164
|
+
label: existingSummary.label,
|
|
165
|
+
};
|
|
166
|
+
const content = yaml.dump(frontmatter).trim() + `${EOL}---${EOL}` + updatedDocumentContent;
|
|
167
|
+
|
|
168
|
+
session.model.changeSet.addElements(this.fileChangeFactory({
|
|
169
|
+
uri: existingSummary.uri,
|
|
170
|
+
type: 'modify',
|
|
171
|
+
state: 'pending',
|
|
172
|
+
targetState: content,
|
|
173
|
+
requestId: session.model.id, // not a request id, as no changeRequest made yet.
|
|
174
|
+
chatSessionId: session.id
|
|
175
|
+
}));
|
|
176
|
+
} else {
|
|
177
|
+
const updatedSummary: Summary = {
|
|
178
|
+
...existingSummary,
|
|
179
|
+
summary: updatedDocumentContent
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Store the updated summary
|
|
183
|
+
await this.storageService.store(updatedSummary);
|
|
184
|
+
}
|
|
185
|
+
return existingSummary.id;
|
|
186
|
+
} else {
|
|
187
|
+
// Fall back to standard update if no document path is found
|
|
188
|
+
const updatedSummaryText = await this.getLlmSummary(session, prompt, agent);
|
|
189
|
+
const updatedSummary: Summary = {
|
|
190
|
+
...existingSummary,
|
|
191
|
+
summary: updatedSummaryText
|
|
192
|
+
};
|
|
193
|
+
await this.storageService.store(updatedSummary);
|
|
194
|
+
return updatedSummary.id;
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
const errorSummary: Summary = {
|
|
198
|
+
...existingSummary,
|
|
199
|
+
summary: `Summary update failed: ${err instanceof Error ? err.message : typeof err === 'string' ? err : 'Unknown error'}`
|
|
200
|
+
};
|
|
201
|
+
await this.storageService.store(errorSummary);
|
|
202
|
+
throw err;
|
|
203
|
+
} finally {
|
|
204
|
+
progress.cancel();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
protected async getLlmSummary(session: ChatSession, prompt: ResolvedPromptFragment | undefined, agent?: ChatAgent): Promise<string> {
|
|
209
|
+
if (!prompt) { return ''; }
|
|
108
210
|
agent = agent || this.agentService.getAgents().find<ChatAgent>((candidate): candidate is ChatAgent =>
|
|
109
211
|
'invoke' in candidate
|
|
110
212
|
&& typeof candidate.invoke === 'function'
|
|
@@ -112,8 +214,7 @@ export class TaskContextService {
|
|
|
112
214
|
);
|
|
113
215
|
if (!agent) { throw new Error('Unable to identify agent for summary.'); }
|
|
114
216
|
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
115
|
-
|
|
116
|
-
if (!prompt) { return ''; }
|
|
217
|
+
|
|
117
218
|
const messages = session.model.getRequests().filter((candidate): candidate is MutableChatRequestModel => candidate instanceof MutableChatRequestModel);
|
|
118
219
|
messages.forEach(message => model['_hierarchy'].append(message));
|
|
119
220
|
const summaryRequest = model.addRequest({
|
|
@@ -126,6 +227,11 @@ export class TaskContextService {
|
|
|
126
227
|
return summaryRequest.response.response.asDisplayString();
|
|
127
228
|
}
|
|
128
229
|
|
|
230
|
+
protected async getSystemPrompt(session: ChatSession, promptId: string = CHAT_SESSION_SUMMARY_PROMPT.id): Promise<ResolvedPromptFragment | undefined> {
|
|
231
|
+
const prompt = await this.promptService.getResolvedPromptFragment(promptId || CHAT_SESSION_SUMMARY_PROMPT.id, undefined, { model: session.model });
|
|
232
|
+
return prompt;
|
|
233
|
+
}
|
|
234
|
+
|
|
129
235
|
hasSummary(chatSession: ChatSession): boolean {
|
|
130
236
|
return !!this.getSummaryForSession(chatSession);
|
|
131
237
|
}
|
|
@@ -28,6 +28,10 @@ export class InMemoryTaskContextStorage implements TaskContextStorageService {
|
|
|
28
28
|
protected readonly onDidChangeEmitter = new Emitter<void>();
|
|
29
29
|
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
30
30
|
|
|
31
|
+
protected sanitizeLabel(label: string): string {
|
|
32
|
+
return label.replace(/^[^\p{L}\p{N}]+/vg, '');
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
@inject(AIVariableResourceResolver)
|
|
32
36
|
protected readonly variableResourceResolver: AIVariableResourceResolver;
|
|
33
37
|
|
|
@@ -35,7 +39,7 @@ export class InMemoryTaskContextStorage implements TaskContextStorageService {
|
|
|
35
39
|
protected readonly openerService: OpenerService;
|
|
36
40
|
|
|
37
41
|
store(summary: Summary): void {
|
|
38
|
-
this.summaries.set(summary.id, summary);
|
|
42
|
+
this.summaries.set(summary.id, { ...summary, label: this.sanitizeLabel(summary.label) });
|
|
39
43
|
this.onDidChangeEmitter.fire();
|
|
40
44
|
}
|
|
41
45
|
|
package/src/common/change-set.ts
CHANGED
|
@@ -142,7 +142,7 @@ export class ChangeSetImpl implements ChangeSet {
|
|
|
142
142
|
const added = [];
|
|
143
143
|
const modified = [];
|
|
144
144
|
const removed = [];
|
|
145
|
-
const toHandle = new Set(
|
|
145
|
+
const toHandle = new Set(this._elements.keys());
|
|
146
146
|
for (const element of elements) {
|
|
147
147
|
toHandle.delete(element.uri.toString());
|
|
148
148
|
if (this.doAdd(element)) {
|
|
@@ -25,6 +25,7 @@ import { ChatAgent } from './chat-agents';
|
|
|
25
25
|
import { AgentService } from '@theia/ai-core';
|
|
26
26
|
|
|
27
27
|
export const ChatAgentService = Symbol('ChatAgentService');
|
|
28
|
+
export const ChatAgentServiceFactory = Symbol('ChatAgentServiceFactory');
|
|
28
29
|
/**
|
|
29
30
|
* The ChatAgentService provides access to the available chat agents.
|
|
30
31
|
*/
|
|
@@ -54,18 +54,19 @@ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inv
|
|
|
54
54
|
import { ChatAgentService } from './chat-agent-service';
|
|
55
55
|
import {
|
|
56
56
|
ChatModel,
|
|
57
|
-
|
|
57
|
+
ChatRequestModel,
|
|
58
58
|
ChatResponseContent,
|
|
59
59
|
ErrorChatResponseContentImpl,
|
|
60
60
|
MarkdownChatResponseContentImpl,
|
|
61
|
-
|
|
62
|
-
ChatRequestModel,
|
|
61
|
+
MutableChatRequestModel,
|
|
63
62
|
ThinkingChatResponseContentImpl,
|
|
63
|
+
ToolCallChatResponseContentImpl,
|
|
64
64
|
ErrorChatResponseContent,
|
|
65
65
|
} from './chat-model';
|
|
66
|
+
import { ChatToolRequest, ChatToolRequestService } from './chat-tool-request-service';
|
|
66
67
|
import { parseContents } from './parse-contents';
|
|
67
68
|
import { DefaultResponseContentFactory, ResponseContentMatcher, ResponseContentMatcherProvider } from './response-content-matcher';
|
|
68
|
-
import {
|
|
69
|
+
import { ImageContextVariable } from './image-context-variable';
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* System message content, enriched with function descriptions.
|
|
@@ -254,11 +255,27 @@ export abstract class AbstractChatAgent implements ChatAgent {
|
|
|
254
255
|
const requestMessages = model.getRequests().flatMap(request => {
|
|
255
256
|
const messages: LanguageModelMessage[] = [];
|
|
256
257
|
const text = request.message.parts.map(part => part.promptText).join('');
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
258
|
+
if (text.length > 0) {
|
|
259
|
+
messages.push({
|
|
260
|
+
actor: 'user',
|
|
261
|
+
type: 'text',
|
|
262
|
+
text: text,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const imageMessages = request.context.variables
|
|
266
|
+
.filter(variable => ImageContextVariable.isResolvedImageContext(variable))
|
|
267
|
+
.map(variable => ImageContextVariable.parseResolved(variable))
|
|
268
|
+
.filter(content => content !== undefined)
|
|
269
|
+
.map(content => ({
|
|
270
|
+
actor: 'user' as const,
|
|
271
|
+
type: 'image' as const,
|
|
272
|
+
image: {
|
|
273
|
+
base64data: content!.data,
|
|
274
|
+
mimeType: content!.mimeType
|
|
275
|
+
}
|
|
276
|
+
}));
|
|
277
|
+
messages.push(...imageMessages);
|
|
278
|
+
|
|
262
279
|
if (request.response.isComplete || includeResponseInProgress) {
|
|
263
280
|
const responseMessages: LanguageModelMessage[] = request.response.response.content
|
|
264
281
|
.filter(c => !ErrorChatResponseContent.is(c))
|
package/src/common/chat-model.ts
CHANGED
|
@@ -19,15 +19,24 @@
|
|
|
19
19
|
*--------------------------------------------------------------------------------------------*/
|
|
20
20
|
// Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
AIVariableResolutionRequest,
|
|
24
|
+
LanguageModelMessage,
|
|
25
|
+
ResolvedAIContextVariable,
|
|
26
|
+
TextMessage,
|
|
27
|
+
ThinkingMessage,
|
|
28
|
+
ToolCallResult,
|
|
29
|
+
ToolResultMessage,
|
|
30
|
+
ToolUseMessage
|
|
31
|
+
} from '@theia/ai-core';
|
|
23
32
|
import { ArrayUtils, CancellationToken, CancellationTokenSource, Command, Disposable, DisposableCollection, Emitter, Event, generateUuid, URI } from '@theia/core';
|
|
24
33
|
import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
|
|
25
34
|
import { Position } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
35
|
+
import { ChangeSet, ChangeSetElement, ChangeSetImpl, ChatUpdateChangeSetEvent } from './change-set';
|
|
26
36
|
import { ChatAgentLocation } from './chat-agents';
|
|
27
37
|
import { ParsedChatRequest } from './parsed-chat-request';
|
|
28
|
-
import { ChangeSet, ChangeSetImpl, ChangeSetElement, ChatUpdateChangeSetEvent } from './change-set';
|
|
29
38
|
import debounce = require('@theia/core/shared/lodash.debounce');
|
|
30
|
-
export { ChangeSet,
|
|
39
|
+
export { ChangeSet, ChangeSetElement, ChangeSetImpl };
|
|
31
40
|
|
|
32
41
|
/**********************
|
|
33
42
|
* INTERFACES AND TYPE GUARDS
|
|
@@ -381,7 +390,7 @@ export interface ToolCallChatResponseContent extends Required<ChatResponseConten
|
|
|
381
390
|
name?: string;
|
|
382
391
|
arguments?: string;
|
|
383
392
|
finished: boolean;
|
|
384
|
-
result?:
|
|
393
|
+
result?: ToolCallResult;
|
|
385
394
|
confirmed: Promise<boolean>;
|
|
386
395
|
confirm(): void;
|
|
387
396
|
deny(): void;
|
|
@@ -1489,12 +1498,12 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
|
|
|
1489
1498
|
protected _name?: string;
|
|
1490
1499
|
protected _arguments?: string;
|
|
1491
1500
|
protected _finished?: boolean;
|
|
1492
|
-
protected _result?:
|
|
1501
|
+
protected _result?: ToolCallResult;
|
|
1493
1502
|
protected _confirmed: Promise<boolean>;
|
|
1494
1503
|
protected _confirmationResolver?: (value: boolean) => void;
|
|
1495
1504
|
protected _confirmationRejecter?: (reason?: unknown) => void;
|
|
1496
1505
|
|
|
1497
|
-
constructor(id?: string, name?: string, arg_string?: string, finished?: boolean, result?:
|
|
1506
|
+
constructor(id?: string, name?: string, arg_string?: string, finished?: boolean, result?: ToolCallResult) {
|
|
1498
1507
|
this._id = id;
|
|
1499
1508
|
this._name = name;
|
|
1500
1509
|
this._arguments = arg_string;
|
|
@@ -1519,7 +1528,7 @@ export class ToolCallChatResponseContentImpl implements ToolCallChatResponseCont
|
|
|
1519
1528
|
get finished(): boolean {
|
|
1520
1529
|
return this._finished === undefined ? false : this._finished;
|
|
1521
1530
|
}
|
|
1522
|
-
get result():
|
|
1531
|
+
get result(): ToolCallResult | undefined {
|
|
1523
1532
|
return this._result;
|
|
1524
1533
|
}
|
|
1525
1534
|
|
|
@@ -104,6 +104,9 @@ export class ChatRequestParserImpl implements ChatRequestParser {
|
|
|
104
104
|
const parts: ParsedChatRequestPart[] = [];
|
|
105
105
|
const variables = new Map<string, AIVariable>();
|
|
106
106
|
const toolRequests = new Map<string, ToolRequest>();
|
|
107
|
+
if (!request.text) {
|
|
108
|
+
return { parts, toolRequests, variables };
|
|
109
|
+
}
|
|
107
110
|
const message = request.text;
|
|
108
111
|
for (let i = 0; i < message.length; i++) {
|
|
109
112
|
const previousChar = message.charAt(i - 1);
|
|
@@ -225,18 +228,6 @@ export class ChatRequestParserImpl implements ChatRequestParser {
|
|
|
225
228
|
return;
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
// The agent must come first
|
|
229
|
-
if (
|
|
230
|
-
parts.some(
|
|
231
|
-
p =>
|
|
232
|
-
(p instanceof ParsedChatRequestTextPart &&
|
|
233
|
-
p.text.trim() !== '') ||
|
|
234
|
-
!(p instanceof ParsedChatRequestAgentPart)
|
|
235
|
-
)
|
|
236
|
-
) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
231
|
return new ParsedChatRequestAgentPart(agentRange, agent.id, agent.name);
|
|
241
232
|
}
|
|
242
233
|
|
|
@@ -21,24 +21,24 @@
|
|
|
21
21
|
|
|
22
22
|
import { AIVariableResolutionRequest, AIVariableService, ResolvedAIContextVariable } from '@theia/ai-core';
|
|
23
23
|
import { Emitter, ILogger, URI, generateUuid } from '@theia/core';
|
|
24
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
24
25
|
import { inject, injectable, optional } from '@theia/core/shared/inversify';
|
|
25
26
|
import { Event } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
26
27
|
import { ChatAgentService } from './chat-agent-service';
|
|
27
28
|
import { ChatAgent, ChatAgentLocation, ChatSessionContext } from './chat-agents';
|
|
28
29
|
import {
|
|
30
|
+
ChatContext,
|
|
29
31
|
ChatModel,
|
|
30
|
-
MutableChatModel,
|
|
31
32
|
ChatRequest,
|
|
32
33
|
ChatRequestModel,
|
|
33
34
|
ChatResponseModel,
|
|
34
35
|
ErrorChatResponseModel,
|
|
35
|
-
|
|
36
|
+
MutableChatModel,
|
|
36
37
|
MutableChatRequestModel,
|
|
37
38
|
} from './chat-model';
|
|
38
39
|
import { ChatRequestParser } from './chat-request-parser';
|
|
39
|
-
import { ParsedChatRequest, ParsedChatRequestAgentPart } from './parsed-chat-request';
|
|
40
40
|
import { ChatSessionNamingService } from './chat-session-naming-service';
|
|
41
|
-
import {
|
|
41
|
+
import { ParsedChatRequest, ParsedChatRequestAgentPart } from './parsed-chat-request';
|
|
42
42
|
|
|
43
43
|
export interface ChatRequestInvocation {
|
|
44
44
|
/**
|
|
@@ -119,6 +119,7 @@ export const PinChatAgent = Symbol('PinChatAgent');
|
|
|
119
119
|
export type PinChatAgent = boolean;
|
|
120
120
|
|
|
121
121
|
export const ChatService = Symbol('ChatService');
|
|
122
|
+
export const ChatServiceFactory = Symbol('ChatServiceFactory');
|
|
122
123
|
export interface ChatService {
|
|
123
124
|
onSessionEvent: Event<ActiveSessionChangedEvent | SessionCreatedEvent | SessionDeletedEvent>
|
|
124
125
|
|
|
@@ -312,15 +313,44 @@ export class ChatServiceImpl implements ChatService {
|
|
|
312
313
|
}
|
|
313
314
|
|
|
314
315
|
protected getAgent(parsedRequest: ParsedChatRequest, session: ChatSession): ChatAgent | undefined {
|
|
315
|
-
|
|
316
|
-
if (this.
|
|
316
|
+
const agent = this.initialAgentSelection(parsedRequest);
|
|
317
|
+
if (!this.isPinChatAgentEnabled()) {
|
|
317
318
|
return agent;
|
|
318
319
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
320
|
+
|
|
321
|
+
return this.handlePinnedAgent(parsedRequest, session, agent);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Determines if chat agent pinning is enabled.
|
|
326
|
+
* Can be overridden by subclasses to provide different logic (e.g., using preferences).
|
|
327
|
+
*/
|
|
328
|
+
protected isPinChatAgentEnabled(): boolean {
|
|
329
|
+
return this.pinChatAgent !== false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Handle pinned agent by:
|
|
334
|
+
* - checking if an agent is pinned, and use it if no other agent is mentioned
|
|
335
|
+
* - pinning the current agent
|
|
336
|
+
*/
|
|
337
|
+
protected handlePinnedAgent(parsedRequest: ParsedChatRequest, session: ChatSession, agent: ChatAgent | undefined): ChatAgent | undefined {
|
|
338
|
+
const mentionedAgentPart = this.getMentionedAgent(parsedRequest);
|
|
339
|
+
const mentionedAgent = mentionedAgentPart ? this.chatAgentService.getAgent(mentionedAgentPart.agentId) : undefined;
|
|
340
|
+
if (mentionedAgent) {
|
|
341
|
+
// If an agent is explicitly mentioned, it becomes the new pinned agent
|
|
342
|
+
session.pinnedAgent = mentionedAgent;
|
|
343
|
+
return mentionedAgent;
|
|
344
|
+
} else if (session.pinnedAgent) {
|
|
345
|
+
// If we have a valid pinned agent, use it (pinned agent may become stale
|
|
346
|
+
// if it was disabled; so we always need to recheck)
|
|
347
|
+
const pinnedAgent = this.chatAgentService.getAgent(session.pinnedAgent.id);
|
|
348
|
+
if (pinnedAgent) {
|
|
349
|
+
return pinnedAgent;
|
|
350
|
+
}
|
|
323
351
|
}
|
|
352
|
+
// Otherwise, override the pinned agent and return the suggested one
|
|
353
|
+
session.pinnedAgent = agent;
|
|
324
354
|
return agent;
|
|
325
355
|
}
|
|
326
356
|
|
|
@@ -30,9 +30,9 @@ import { ChatSession } from './chat-service';
|
|
|
30
30
|
import { generateUuid } from '@theia/core';
|
|
31
31
|
|
|
32
32
|
const CHAT_SESSION_NAMING_PROMPT: PromptVariantSet = {
|
|
33
|
-
id: 'chat-session-naming-
|
|
33
|
+
id: 'chat-session-naming-system',
|
|
34
34
|
defaultVariant: {
|
|
35
|
-
id: 'chat-session-naming-
|
|
35
|
+
id: 'chat-session-naming-system-default',
|
|
36
36
|
template: '{{!-- Made improvements or adaptations to this prompt template? We\'d love for you to share it with the community! Contribute back here: ' +
|
|
37
37
|
'https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}\n\n' +
|
|
38
38
|
'Provide a short and descriptive name for the given AI chat conversation of an AI-powered tool based on the conversation below.\n\n' +
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
import { CHANGE_SET_SUMMARY_VARIABLE_ID } from './context-variables';
|
|
12
12
|
|
|
13
13
|
export const CHAT_SESSION_SUMMARY_PROMPT = {
|
|
14
|
-
id: 'chat-session-summary-system
|
|
14
|
+
id: 'chat-session-summary-system',
|
|
15
15
|
defaultVariant: {
|
|
16
|
-
id: 'chat-session-summary-
|
|
16
|
+
id: 'chat-session-summary-system-default',
|
|
17
17
|
template: '{{!-- !-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).\n' +
|
|
18
18
|
'Made improvements or adaptations to this prompt template? We\'d love for you to share it with the community! Contribute back here: ' +
|
|
19
19
|
'https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}\n\n' +
|
|
@@ -23,7 +23,7 @@ export const CHAT_SESSION_SUMMARY_PROMPT = {
|
|
|
23
23
|
'Ensure that the summary is sufficiently comprehensive to allow seamless continuation of the workflow. ' +
|
|
24
24
|
'The summary will primarily be used by other AI agents, so tailor your response for use by AI agents. ' +
|
|
25
25
|
'Also consider the system message. ' +
|
|
26
|
-
'Make sure you include all necessary context information and use unique references(such as URIs, file paths, etc.). ' +
|
|
26
|
+
'Make sure you include all necessary context information and use unique references (such as URIs, file paths, etc.). ' +
|
|
27
27
|
'If the conversation was about a task, describe the state of the task, i.e.what has been completed and what is open. ' +
|
|
28
28
|
'If a changeset is open in the session, describe the state of the suggested changes. ' +
|
|
29
29
|
`\n\n{{${CHANGE_SET_SUMMARY_VARIABLE_ID}}}`,
|