@theia/ai-chat 1.60.2 → 1.61.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/ai-chat-frontend-contribution.d.ts +11 -0
- package/lib/browser/ai-chat-frontend-contribution.d.ts.map +1 -0
- package/lib/browser/ai-chat-frontend-contribution.js +56 -0
- package/lib/browser/ai-chat-frontend-contribution.js.map +1 -0
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +21 -3
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-decorator-service.d.ts +24 -0
- package/lib/browser/change-set-decorator-service.d.ts.map +1 -0
- package/lib/browser/change-set-decorator-service.js +66 -0
- package/lib/browser/change-set-decorator-service.js.map +1 -0
- package/lib/browser/change-set-file-element.d.ts +7 -4
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +20 -12
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/change-set-file-resource.d.ts +1 -42
- package/lib/browser/change-set-file-resource.d.ts.map +1 -1
- package/lib/browser/change-set-file-resource.js +1 -136
- package/lib/browser/change-set-file-resource.js.map +1 -1
- package/lib/browser/change-set-variable.d.ts.map +1 -1
- package/lib/browser/change-set-variable.js +13 -4
- package/lib/browser/change-set-variable.js.map +1 -1
- package/lib/browser/context-file-variable-label-provider.js +1 -1
- package/lib/browser/context-file-variable-label-provider.js.map +1 -1
- package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +29 -27
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/task-context-service.d.ts +40 -0
- package/lib/browser/task-context-service.d.ts.map +1 -0
- package/lib/browser/task-context-service.js +148 -0
- package/lib/browser/task-context-service.js.map +1 -0
- package/lib/browser/task-context-storage-service.d.ts +18 -0
- package/lib/browser/task-context-storage-service.d.ts.map +1 -0
- package/lib/browser/task-context-storage-service.js +77 -0
- package/lib/browser/task-context-storage-service.js.map +1 -0
- package/lib/browser/task-context-variable-contribution.d.ts +20 -0
- package/lib/browser/task-context-variable-contribution.d.ts.map +1 -0
- package/lib/browser/task-context-variable-contribution.js +101 -0
- package/lib/browser/task-context-variable-contribution.js.map +1 -0
- package/lib/browser/task-context-variable-label-provider.d.ts +21 -0
- package/lib/browser/task-context-variable-label-provider.d.ts.map +1 -0
- package/lib/browser/task-context-variable-label-provider.js +83 -0
- package/lib/browser/task-context-variable-label-provider.js.map +1 -0
- package/lib/browser/task-context-variable.d.ts +3 -0
- package/lib/browser/task-context-variable.d.ts.map +1 -0
- package/lib/browser/task-context-variable.js +29 -0
- package/lib/browser/task-context-variable.js.map +1 -0
- package/lib/common/chat-agents.d.ts +2 -1
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +32 -20
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-model.d.ts +191 -8
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +369 -12
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts +1 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +1 -1
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-service.d.ts +2 -0
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +18 -25
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-naming-service.d.ts.map +1 -1
- package/lib/common/chat-session-naming-service.js +9 -11
- package/lib/common/chat-session-naming-service.js.map +1 -1
- package/lib/common/chat-session-summary-agent-prompt.d.ts +5 -0
- package/lib/common/chat-session-summary-agent-prompt.d.ts.map +1 -0
- package/lib/common/chat-session-summary-agent-prompt.js +30 -0
- package/lib/common/chat-session-summary-agent-prompt.js.map +1 -0
- package/lib/common/chat-session-summary-agent.d.ts +17 -0
- package/lib/common/chat-session-summary-agent.d.ts.map +1 -0
- package/lib/common/chat-session-summary-agent.js +48 -0
- package/lib/common/chat-session-summary-agent.js.map +1 -0
- package/lib/common/context-summary-variable.js +1 -1
- package/lib/common/context-summary-variable.js.map +1 -1
- package/lib/common/parse-contents-with-incomplete-parts.spec.d.ts +2 -0
- package/lib/common/parse-contents-with-incomplete-parts.spec.d.ts.map +1 -0
- package/lib/common/parse-contents-with-incomplete-parts.spec.js +103 -0
- package/lib/common/parse-contents-with-incomplete-parts.spec.js.map +1 -0
- package/lib/common/parse-contents.d.ts +1 -0
- package/lib/common/parse-contents.d.ts.map +1 -1
- package/lib/common/parse-contents.js +45 -5
- package/lib/common/parse-contents.js.map +1 -1
- package/lib/common/parse-contents.spec.d.ts +1 -0
- package/lib/common/parse-contents.spec.d.ts.map +1 -1
- package/lib/common/parse-contents.spec.js +25 -13
- package/lib/common/parse-contents.spec.js.map +1 -1
- package/lib/common/response-content-matcher.d.ts +6 -0
- package/lib/common/response-content-matcher.d.ts.map +1 -1
- package/lib/common/response-content-matcher.js +14 -3
- package/lib/common/response-content-matcher.js.map +1 -1
- package/package.json +11 -11
- package/src/browser/ai-chat-frontend-contribution.ts +49 -0
- package/src/browser/ai-chat-frontend-module.ts +25 -4
- package/src/browser/change-set-decorator-service.ts +72 -0
- package/src/browser/change-set-file-element.ts +18 -13
- package/src/browser/change-set-file-resource.ts +1 -138
- package/src/browser/change-set-variable.ts +14 -6
- package/src/browser/context-file-variable-label-provider.ts +1 -1
- package/src/browser/file-chat-variable-contribution.ts +26 -29
- package/src/browser/task-context-service.ts +144 -0
- package/src/browser/task-context-storage-service.ts +75 -0
- package/src/browser/task-context-variable-contribution.ts +93 -0
- package/src/browser/task-context-variable-label-provider.ts +67 -0
- package/src/browser/task-context-variable.ts +28 -0
- package/src/common/chat-agents.ts +38 -22
- package/src/common/chat-model.ts +566 -17
- package/src/common/chat-request-parser.ts +2 -2
- package/src/common/chat-service.ts +17 -26
- package/src/common/chat-session-naming-service.ts +13 -15
- package/src/common/chat-session-summary-agent-prompt.ts +28 -0
- package/src/common/chat-session-summary-agent.ts +42 -0
- package/src/common/context-summary-variable.ts +1 -1
- package/src/common/parse-contents-with-incomplete-parts.spec.ts +114 -0
- package/src/common/parse-contents.spec.ts +24 -12
- package/src/common/parse-contents.ts +52 -6
- package/src/common/response-content-matcher.ts +21 -3
|
@@ -55,7 +55,7 @@ function offsetRange(start: number, endExclusive: number): OffsetRange {
|
|
|
55
55
|
return { start, endExclusive };
|
|
56
56
|
}
|
|
57
57
|
@injectable()
|
|
58
|
-
export class ChatRequestParserImpl {
|
|
58
|
+
export class ChatRequestParserImpl implements ChatRequestParser {
|
|
59
59
|
constructor(
|
|
60
60
|
@inject(ChatAgentService) private readonly agentService: ChatAgentService,
|
|
61
61
|
@inject(AIVariableService) private readonly variableService: AIVariableService,
|
|
@@ -90,7 +90,7 @@ export class ChatRequestParserImpl {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
// Get resolved variables from variable cache after all variables have been resolved.
|
|
93
|
-
// We want to return all
|
|
93
|
+
// We want to return all recursively resolved variables, thus use the whole cache.
|
|
94
94
|
const resolvedVariables = await getAllResolvedAIVariables(variableCache);
|
|
95
95
|
|
|
96
96
|
return { request, parts, toolRequests, variables: resolvedVariables };
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { ChatRequestParser } from './chat-request-parser';
|
|
39
39
|
import { ParsedChatRequest, ParsedChatRequestAgentPart } from './parsed-chat-request';
|
|
40
40
|
import { ChatSessionNamingService } from './chat-session-naming-service';
|
|
41
|
+
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
41
42
|
|
|
42
43
|
export interface ChatRequestInvocation {
|
|
43
44
|
/**
|
|
@@ -125,6 +126,7 @@ export interface ChatService {
|
|
|
125
126
|
getSessions(): ChatSession[];
|
|
126
127
|
createSession(location?: ChatAgentLocation, options?: SessionOptions, pinnedAgent?: ChatAgent): ChatSession;
|
|
127
128
|
deleteSession(sessionId: string): void;
|
|
129
|
+
getActiveSession(): ChatSession | undefined;
|
|
128
130
|
setActiveSession(sessionId: string, options?: SessionOptions): void;
|
|
129
131
|
|
|
130
132
|
sendRequest(
|
|
@@ -208,6 +210,12 @@ export class ChatServiceImpl implements ChatService {
|
|
|
208
210
|
this.onSessionEventEmitter.fire({ type: 'deleted', sessionId: sessionId });
|
|
209
211
|
}
|
|
210
212
|
|
|
213
|
+
getActiveSession(): ChatSession | undefined {
|
|
214
|
+
const activeSessions = this._sessions.filter(candidate => candidate.isActive);
|
|
215
|
+
if (activeSessions.length > 1) { throw new Error('More than one session marked as active. This indicates an error in ChatService.'); }
|
|
216
|
+
return activeSessions.at(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
211
219
|
setActiveSession(sessionId: string | undefined, options?: SessionOptions): void {
|
|
212
220
|
this._sessions.forEach(session => {
|
|
213
221
|
session.isActive = session.id === sessionId;
|
|
@@ -225,7 +233,7 @@ export class ChatServiceImpl implements ChatService {
|
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
const resolutionContext: ChatSessionContext = { model: session.model };
|
|
228
|
-
const resolvedContext = await this.resolveChatContext(session.model.context.getVariables(), resolutionContext);
|
|
236
|
+
const resolvedContext = await this.resolveChatContext(request.variables ?? session.model.context.getVariables(), resolutionContext);
|
|
229
237
|
const parsedRequest = await this.chatRequestParser.parseChatRequest(request, session.model.location, resolvedContext);
|
|
230
238
|
const agent = this.getAgent(parsedRequest, session);
|
|
231
239
|
|
|
@@ -244,33 +252,23 @@ export class ChatServiceImpl implements ChatService {
|
|
|
244
252
|
this.updateSessionMetadata(session, requestModel);
|
|
245
253
|
resolutionContext.request = requestModel;
|
|
246
254
|
|
|
247
|
-
|
|
248
|
-
let resolveResponseCompleted: (responseModel: ChatResponseModel) => void;
|
|
255
|
+
const responseCompletionDeferred = new Deferred<ChatResponseModel>();
|
|
249
256
|
const invocation: ChatRequestInvocation = {
|
|
250
257
|
requestCompleted: Promise.resolve(requestModel),
|
|
251
|
-
responseCreated:
|
|
252
|
-
|
|
253
|
-
}),
|
|
254
|
-
responseCompleted: new Promise(resolve => {
|
|
255
|
-
resolveResponseCompleted = resolve;
|
|
256
|
-
}),
|
|
258
|
+
responseCreated: Promise.resolve(requestModel.response),
|
|
259
|
+
responseCompleted: responseCompletionDeferred.promise,
|
|
257
260
|
};
|
|
258
261
|
|
|
259
|
-
resolveResponseCreated!(requestModel.response);
|
|
260
262
|
requestModel.response.onDidChange(() => {
|
|
261
263
|
if (requestModel.response.isComplete) {
|
|
262
|
-
|
|
264
|
+
responseCompletionDeferred.resolve(requestModel.response);
|
|
263
265
|
}
|
|
264
266
|
if (requestModel.response.isError) {
|
|
265
|
-
|
|
267
|
+
responseCompletionDeferred.resolve(requestModel.response);
|
|
266
268
|
}
|
|
267
269
|
});
|
|
268
270
|
|
|
269
|
-
|
|
270
|
-
agent.invoke(requestModel).catch(error => requestModel.response.error(error));
|
|
271
|
-
} else {
|
|
272
|
-
this.logger.error('No ChatAgents available to handle request!', requestModel);
|
|
273
|
-
}
|
|
271
|
+
agent.invoke(requestModel).catch(error => requestModel.response.error(error));
|
|
274
272
|
|
|
275
273
|
return invocation;
|
|
276
274
|
}
|
|
@@ -304,15 +302,8 @@ export class ChatServiceImpl implements ChatService {
|
|
|
304
302
|
context: ChatSessionContext,
|
|
305
303
|
): Promise<ChatContext> {
|
|
306
304
|
// TODO use a common cache to resolve variables and return recursively resolved variables?
|
|
307
|
-
const resolvedVariables = await Promise.all(
|
|
308
|
-
|
|
309
|
-
const resolvedVariable = await this.variableService.resolveVariable(contextVariable, context);
|
|
310
|
-
if (ResolvedAIContextVariable.is(resolvedVariable)) {
|
|
311
|
-
return resolvedVariable;
|
|
312
|
-
}
|
|
313
|
-
return undefined;
|
|
314
|
-
})
|
|
315
|
-
).then(results => results.filter((result): result is ResolvedAIContextVariable => result !== undefined));
|
|
305
|
+
const resolvedVariables = await Promise.all(resolutionRequests.map(async contextVariable => this.variableService.resolveVariable(contextVariable, context)))
|
|
306
|
+
.then(results => results.filter(ResolvedAIContextVariable.is));
|
|
316
307
|
return { variables: resolvedVariables };
|
|
317
308
|
}
|
|
318
309
|
|
|
@@ -18,12 +18,13 @@ import {
|
|
|
18
18
|
Agent,
|
|
19
19
|
AgentService,
|
|
20
20
|
CommunicationRecordingService,
|
|
21
|
+
CommunicationRequestEntryParam,
|
|
21
22
|
getTextOfResponse,
|
|
22
23
|
LanguageModelRegistry,
|
|
23
|
-
LanguageModelRequest,
|
|
24
24
|
LanguageModelRequirement,
|
|
25
25
|
PromptService,
|
|
26
|
-
PromptTemplate
|
|
26
|
+
PromptTemplate,
|
|
27
|
+
UserRequest
|
|
27
28
|
} from '@theia/ai-core';
|
|
28
29
|
import { inject, injectable } from '@theia/core/shared/inversify';
|
|
29
30
|
import { ChatSession } from './chat-service';
|
|
@@ -38,7 +39,7 @@ const CHAT_SESSION_NAMING_PROMPT = {
|
|
|
38
39
|
'Use the same language for the chat conversation name as used in the provided conversation, if in doubt default to English. ' +
|
|
39
40
|
'Start the chat conversation name with an upper-case letter. ' +
|
|
40
41
|
'Below we also provide the already existing other conversation names, make sure your suggestion for a name is unique with respect to the existing ones.\n\n' +
|
|
41
|
-
'IMPORTANT: Your answer MUST ONLY CONTAIN THE PROPOSED NAME and must not be preceded or
|
|
42
|
+
'IMPORTANT: Your answer MUST ONLY CONTAIN THE PROPOSED NAME and must not be preceded or followed by any other text.' +
|
|
42
43
|
'\n\nOther session names:\n{{listOfSessionNames}}' +
|
|
43
44
|
'\n\nConversation:\n{{conversation}}',
|
|
44
45
|
};
|
|
@@ -92,7 +93,7 @@ export class ChatSessionNamingAgent implements Agent {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
const conversation = chatSession.model.getRequests()
|
|
95
|
-
.map(req => `<user>${req.
|
|
96
|
+
.map(req => `<user>${req.message.parts.map(chunk => chunk.promptText).join('')}</user>` +
|
|
96
97
|
(req.response.response ? `<assistant>${req.response.response.asString()}</assistant>` : ''))
|
|
97
98
|
.join('\n\n');
|
|
98
99
|
const listOfSessionNames = otherNames.map(name => name).join(', ');
|
|
@@ -103,22 +104,19 @@ export class ChatSessionNamingAgent implements Agent {
|
|
|
103
104
|
throw new Error('Unable to create prompt message for generating chat session name');
|
|
104
105
|
}
|
|
105
106
|
|
|
106
|
-
const
|
|
107
|
+
const sessionId = generateUuid();
|
|
108
|
+
const requestId = generateUuid();
|
|
109
|
+
const request: UserRequest = {
|
|
107
110
|
messages: [{
|
|
108
111
|
actor: 'user',
|
|
109
112
|
text: message,
|
|
110
113
|
type: 'text'
|
|
111
|
-
}]
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const sessionId = generateUuid();
|
|
115
|
-
const requestId = generateUuid();
|
|
116
|
-
this.recordingService.recordRequest({
|
|
117
|
-
agentId: this.id,
|
|
118
|
-
sessionId,
|
|
114
|
+
}],
|
|
119
115
|
requestId,
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
sessionId,
|
|
117
|
+
agentId: this.id
|
|
118
|
+
};
|
|
119
|
+
this.recordingService.recordRequest({ ...request, request: request.messages } satisfies CommunicationRequestEntryParam);
|
|
122
120
|
|
|
123
121
|
const result = await lm.request(request);
|
|
124
122
|
const response = await getTextOfResponse(result);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/tslint/config */
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2025 EclipseSource GmbH and others.
|
|
4
|
+
//
|
|
5
|
+
// This file is licensed under the MIT License.
|
|
6
|
+
// See LICENSE-MIT.txt in the project root for license information.
|
|
7
|
+
// https://opensource.org/license/mit.
|
|
8
|
+
//
|
|
9
|
+
// SPDX-License-Identifier: MIT
|
|
10
|
+
|
|
11
|
+
import { CHANGE_SET_SUMMARY_VARIABLE_ID } from './context-variables';
|
|
12
|
+
|
|
13
|
+
export const CHAT_SESSION_SUMMARY_PROMPT = {
|
|
14
|
+
id: 'chat-session-summary-prompt',
|
|
15
|
+
template: `{{!-- !-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
|
|
16
|
+
Made improvements or adaptations to this prompt template? We\'d love for you to share it with the community! Contribute back here: ' +
|
|
17
|
+
'https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}\n\n' +
|
|
18
|
+
'You are a chat agent for summarizing AI agent chat sessions for later use. \
|
|
19
|
+
Review the conversation above and generate a concise summary that captures every crucial detail, \
|
|
20
|
+
including all requirements, decisions, and pending tasks. \
|
|
21
|
+
Ensure that the summary is sufficiently comprehensive to allow seamless continuation of the workflow. The summary will primarily be used by other AI agents, so tailor your \
|
|
22
|
+
response for use by AI agents. \
|
|
23
|
+
Also consider the system message.
|
|
24
|
+
Make sure you include all necessary context information and use unique references (such as URIs, file paths, etc.).
|
|
25
|
+
If the conversation was about a task, describe the state of the task, i.e. what has been completed and what is open.
|
|
26
|
+
If a changeset is open in the session, describe the state of the suggested changes.
|
|
27
|
+
{{${CHANGE_SET_SUMMARY_VARIABLE_ID}}}`,
|
|
28
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
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
|
+
LanguageModelRequirement,
|
|
19
|
+
PromptTemplate
|
|
20
|
+
} from '@theia/ai-core';
|
|
21
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
22
|
+
import { AbstractStreamParsingChatAgent, ChatAgent } from './chat-agents';
|
|
23
|
+
import { CHAT_SESSION_SUMMARY_PROMPT } from './chat-session-summary-agent-prompt';
|
|
24
|
+
|
|
25
|
+
@injectable()
|
|
26
|
+
export class ChatSessionSummaryAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
|
|
27
|
+
static ID = 'chat-session-summary-agent';
|
|
28
|
+
id = ChatSessionSummaryAgent.ID;
|
|
29
|
+
name = 'Chat Session Summary';
|
|
30
|
+
override description = 'Agent for generating chat session summaries.';
|
|
31
|
+
override variables = [];
|
|
32
|
+
override promptTemplates: PromptTemplate[] = [CHAT_SESSION_SUMMARY_PROMPT];
|
|
33
|
+
protected readonly defaultLanguageModelPurpose = 'chat-session-summary';
|
|
34
|
+
languageModelRequirements: LanguageModelRequirement[] = [{
|
|
35
|
+
purpose: 'chat-session-summary',
|
|
36
|
+
identifier: 'openai/gpt-4o-mini',
|
|
37
|
+
}];
|
|
38
|
+
override agentSpecificVariables = [];
|
|
39
|
+
override functions = [];
|
|
40
|
+
override locations = [];
|
|
41
|
+
override tags = [];
|
|
42
|
+
}
|
|
@@ -38,7 +38,7 @@ export class ContextSummaryVariableContribution implements AIVariableContributio
|
|
|
38
38
|
|
|
39
39
|
async resolve(request: AIVariableResolutionRequest, context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
|
|
40
40
|
if (!ChatSessionContext.is(context) || request.variable.name !== CONTEXT_SUMMARY_VARIABLE.name) { return undefined; }
|
|
41
|
-
const data =
|
|
41
|
+
const data = ChatSessionContext.getVariables(context).filter(variable => variable.variable.isContextVariable)
|
|
42
42
|
.map(variable => ({
|
|
43
43
|
type: variable.variable.name,
|
|
44
44
|
// eslint-disable-next-line no-null/no-null
|
|
@@ -0,0 +1,114 @@
|
|
|
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 { expect } from 'chai';
|
|
18
|
+
import { MutableChatRequestModel, CodeChatResponseContentImpl, MarkdownChatResponseContentImpl } from './chat-model';
|
|
19
|
+
import { parseContents } from './parse-contents';
|
|
20
|
+
import { ResponseContentMatcher } from './response-content-matcher';
|
|
21
|
+
|
|
22
|
+
const fakeRequest = {} as MutableChatRequestModel;
|
|
23
|
+
|
|
24
|
+
// Custom matchers with incompleteContentFactory for testing
|
|
25
|
+
const TestCodeContentMatcher: ResponseContentMatcher = {
|
|
26
|
+
start: /^```.*?$/m,
|
|
27
|
+
end: /^```$/m,
|
|
28
|
+
contentFactory: (content: string) => {
|
|
29
|
+
const language = content.match(/^```(\w+)/)?.[1] || '';
|
|
30
|
+
const code = content.replace(/^```(\w+)\n|```$/g, '');
|
|
31
|
+
return new CodeChatResponseContentImpl(code.trim(), language);
|
|
32
|
+
},
|
|
33
|
+
incompleteContentFactory: (content: string) => {
|
|
34
|
+
const language = content.match(/^```(\w+)/)?.[1] || '';
|
|
35
|
+
// Remove only the start delimiter, since we don't have an end delimiter yet
|
|
36
|
+
const code = content.replace(/^```(\w+)\n?/g, '');
|
|
37
|
+
return new CodeChatResponseContentImpl(code.trim(), language);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe('parseContents with incomplete parts', () => {
|
|
42
|
+
it('should handle incomplete code blocks with incompleteContentFactory', () => {
|
|
43
|
+
// Only the start of a code block without an end
|
|
44
|
+
const text = '```typescript\nconsole.log("Hello World");';
|
|
45
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
46
|
+
|
|
47
|
+
expect(result.length).to.equal(1);
|
|
48
|
+
expect(result[0]).to.be.instanceOf(CodeChatResponseContentImpl);
|
|
49
|
+
const codeContent = result[0] as CodeChatResponseContentImpl;
|
|
50
|
+
expect(codeContent.code).to.equal('console.log("Hello World");');
|
|
51
|
+
expect(codeContent.language).to.equal('typescript');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle complete code blocks with contentFactory', () => {
|
|
55
|
+
const text = '```typescript\nconsole.log("Hello World");\n```';
|
|
56
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
57
|
+
|
|
58
|
+
expect(result.length).to.equal(1);
|
|
59
|
+
expect(result[0]).to.be.instanceOf(CodeChatResponseContentImpl);
|
|
60
|
+
const codeContent = result[0] as CodeChatResponseContentImpl;
|
|
61
|
+
expect(codeContent.code).to.equal('console.log("Hello World");');
|
|
62
|
+
expect(codeContent.language).to.equal('typescript');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should handle mixed content with incomplete and complete blocks', () => {
|
|
66
|
+
const text = 'Some text\n```typescript\nconsole.log("Hello");\n```\nMore text\n```python\nprint("World")';
|
|
67
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
68
|
+
|
|
69
|
+
expect(result.length).to.equal(4);
|
|
70
|
+
expect(result[0]).to.be.instanceOf(MarkdownChatResponseContentImpl);
|
|
71
|
+
expect(result[1]).to.be.instanceOf(CodeChatResponseContentImpl);
|
|
72
|
+
const completeContent = result[1] as CodeChatResponseContentImpl;
|
|
73
|
+
expect(completeContent.language).to.equal('typescript');
|
|
74
|
+
expect(result[2]).to.be.instanceOf(MarkdownChatResponseContentImpl);
|
|
75
|
+
expect(result[3]).to.be.instanceOf(CodeChatResponseContentImpl);
|
|
76
|
+
const incompleteContent = result[3] as CodeChatResponseContentImpl;
|
|
77
|
+
expect(incompleteContent.language).to.equal('python');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should use default content factory if no incompleteContentFactory provided', () => {
|
|
81
|
+
// Create a matcher without incompleteContentFactory
|
|
82
|
+
const matcherWithoutIncomplete: ResponseContentMatcher = {
|
|
83
|
+
start: /^<test>$/m,
|
|
84
|
+
end: /^<\/test>$/m,
|
|
85
|
+
contentFactory: (content: string) => new MarkdownChatResponseContentImpl('complete: ' + content)
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Text with only the start delimiter
|
|
89
|
+
const text = '<test>\ntest content';
|
|
90
|
+
const result = parseContents(text, fakeRequest, [matcherWithoutIncomplete]);
|
|
91
|
+
|
|
92
|
+
expect(result.length).to.equal(1);
|
|
93
|
+
expect(result[0]).to.be.instanceOf(MarkdownChatResponseContentImpl);
|
|
94
|
+
expect((result[0] as MarkdownChatResponseContentImpl).content.value).to.equal('<test>\ntest content');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should prefer complete matches over incomplete ones', () => {
|
|
98
|
+
// Text with both a complete and incomplete match at same position
|
|
99
|
+
const text = '```typescript\nconsole.log();\n```\n<test>\ntest content';
|
|
100
|
+
const matcherWithoutIncomplete: ResponseContentMatcher = {
|
|
101
|
+
start: /^<test>$/m,
|
|
102
|
+
end: /^<\/test>$/m,
|
|
103
|
+
contentFactory: (content: string) => new MarkdownChatResponseContentImpl('complete: ' + content)
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher, matcherWithoutIncomplete]);
|
|
107
|
+
|
|
108
|
+
expect(result.length).to.equal(2);
|
|
109
|
+
expect(result[0]).to.be.instanceOf(CodeChatResponseContentImpl);
|
|
110
|
+
expect((result[0] as CodeChatResponseContentImpl).language).to.equal('typescript');
|
|
111
|
+
expect(result[1]).to.be.instanceOf(MarkdownChatResponseContentImpl);
|
|
112
|
+
expect((result[1] as MarkdownChatResponseContentImpl).content.value).to.contain('test content');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -19,6 +19,16 @@ import { MutableChatRequestModel, ChatResponseContent, CodeChatResponseContentIm
|
|
|
19
19
|
import { parseContents } from './parse-contents';
|
|
20
20
|
import { CodeContentMatcher, ResponseContentMatcher } from './response-content-matcher';
|
|
21
21
|
|
|
22
|
+
export const TestCodeContentMatcher: ResponseContentMatcher = {
|
|
23
|
+
start: /^```.*?$/m,
|
|
24
|
+
end: /^```$/m,
|
|
25
|
+
contentFactory: (content: string) => {
|
|
26
|
+
const language = content.match(/^```(\w+)/)?.[1] || '';
|
|
27
|
+
const code = content.replace(/^```(\w+)\n|```$/g, '');
|
|
28
|
+
return new CodeChatResponseContentImpl(code.trim(), language);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
22
32
|
export class CommandChatResponseContentImpl implements ChatResponseContent {
|
|
23
33
|
constructor(public readonly command: string) { }
|
|
24
34
|
kind = 'command';
|
|
@@ -38,19 +48,19 @@ const fakeRequest = {} as MutableChatRequestModel;
|
|
|
38
48
|
describe('parseContents', () => {
|
|
39
49
|
it('should parse code content', () => {
|
|
40
50
|
const text = '```typescript\nconsole.log("Hello World");\n```';
|
|
41
|
-
const result = parseContents(text, fakeRequest);
|
|
51
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
42
52
|
expect(result).to.deep.equal([new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript')]);
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
it('should parse markdown content', () => {
|
|
46
56
|
const text = 'Hello **World**';
|
|
47
|
-
const result = parseContents(text, fakeRequest);
|
|
57
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
48
58
|
expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Hello **World**')]);
|
|
49
59
|
});
|
|
50
60
|
|
|
51
61
|
it('should parse multiple content blocks', () => {
|
|
52
62
|
const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**';
|
|
53
|
-
const result = parseContents(text, fakeRequest);
|
|
63
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
54
64
|
expect(result).to.deep.equal([
|
|
55
65
|
new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
|
|
56
66
|
new MarkdownChatResponseContentImpl('\nHello **World**')
|
|
@@ -59,7 +69,7 @@ describe('parseContents', () => {
|
|
|
59
69
|
|
|
60
70
|
it('should parse multiple content blocks with different languages', () => {
|
|
61
71
|
const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")\n```';
|
|
62
|
-
const result = parseContents(text, fakeRequest);
|
|
72
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
63
73
|
expect(result).to.deep.equal([
|
|
64
74
|
new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
|
|
65
75
|
new CodeChatResponseContentImpl('print("Hello World")', 'python')
|
|
@@ -68,7 +78,7 @@ describe('parseContents', () => {
|
|
|
68
78
|
|
|
69
79
|
it('should parse multiple content blocks with different languages and markdown', () => {
|
|
70
80
|
const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**\n```python\nprint("Hello World")\n```';
|
|
71
|
-
const result = parseContents(text, fakeRequest);
|
|
81
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
72
82
|
expect(result).to.deep.equal([
|
|
73
83
|
new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
|
|
74
84
|
new MarkdownChatResponseContentImpl('\nHello **World**\n'),
|
|
@@ -78,7 +88,7 @@ describe('parseContents', () => {
|
|
|
78
88
|
|
|
79
89
|
it('should parse content blocks with empty content', () => {
|
|
80
90
|
const text = '```typescript\n```\nHello **World**\n```python\nprint("Hello World")\n```';
|
|
81
|
-
const result = parseContents(text, fakeRequest);
|
|
91
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
82
92
|
expect(result).to.deep.equal([
|
|
83
93
|
new CodeChatResponseContentImpl('', 'typescript'),
|
|
84
94
|
new MarkdownChatResponseContentImpl('\nHello **World**\n'),
|
|
@@ -88,7 +98,7 @@ describe('parseContents', () => {
|
|
|
88
98
|
|
|
89
99
|
it('should parse content with markdown, code, and markdown', () => {
|
|
90
100
|
const text = 'Hello **World**\n```typescript\nconsole.log("Hello World");\n```\nGoodbye **World**';
|
|
91
|
-
const result = parseContents(text, fakeRequest);
|
|
101
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
92
102
|
expect(result).to.deep.equal([
|
|
93
103
|
new MarkdownChatResponseContentImpl('Hello **World**\n'),
|
|
94
104
|
new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
|
|
@@ -98,34 +108,36 @@ describe('parseContents', () => {
|
|
|
98
108
|
|
|
99
109
|
it('should handle text with no special content', () => {
|
|
100
110
|
const text = 'Just some plain text.';
|
|
101
|
-
const result = parseContents(text, fakeRequest);
|
|
111
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
102
112
|
expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Just some plain text.')]);
|
|
103
113
|
});
|
|
104
114
|
|
|
105
115
|
it('should handle text with only start code block', () => {
|
|
106
116
|
const text = '```typescript\nconsole.log("Hello World");';
|
|
117
|
+
// We're using the standard CodeContentMatcher which has incompleteContentFactory
|
|
107
118
|
const result = parseContents(text, fakeRequest);
|
|
108
|
-
expect(result).to.deep.equal([new
|
|
119
|
+
expect(result).to.deep.equal([new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript')]);
|
|
109
120
|
});
|
|
110
121
|
|
|
111
122
|
it('should handle text with only end code block', () => {
|
|
112
123
|
const text = 'console.log("Hello World");\n```';
|
|
113
|
-
const result = parseContents(text, fakeRequest);
|
|
124
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
114
125
|
expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('console.log("Hello World");\n```')]);
|
|
115
126
|
});
|
|
116
127
|
|
|
117
128
|
it('should handle text with unmatched code block', () => {
|
|
118
129
|
const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")';
|
|
130
|
+
// We're using the standard CodeContentMatcher which has incompleteContentFactory
|
|
119
131
|
const result = parseContents(text, fakeRequest);
|
|
120
132
|
expect(result).to.deep.equal([
|
|
121
133
|
new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
|
|
122
|
-
new
|
|
134
|
+
new CodeChatResponseContentImpl('print("Hello World")', 'python')
|
|
123
135
|
]);
|
|
124
136
|
});
|
|
125
137
|
|
|
126
138
|
it('should parse code block without newline after language', () => {
|
|
127
139
|
const text = '```typescript console.log("Hello World");```';
|
|
128
|
-
const result = parseContents(text, fakeRequest);
|
|
140
|
+
const result = parseContents(text, fakeRequest, [TestCodeContentMatcher]);
|
|
129
141
|
expect(result).to.deep.equal([
|
|
130
142
|
new MarkdownChatResponseContentImpl('```typescript console.log("Hello World");```')
|
|
131
143
|
]);
|
|
@@ -20,6 +20,7 @@ interface Match {
|
|
|
20
20
|
matcher: ResponseContentMatcher;
|
|
21
21
|
index: number;
|
|
22
22
|
content: string;
|
|
23
|
+
isComplete: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function parseContents(
|
|
@@ -50,7 +51,16 @@ export function parseContents(
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
// 2. Add the matched content object
|
|
53
|
-
|
|
54
|
+
if (match.isComplete) {
|
|
55
|
+
// Complete match, use regular content factory
|
|
56
|
+
result.push(match.matcher.contentFactory(match.content, request));
|
|
57
|
+
} else if (match.matcher.incompleteContentFactory) {
|
|
58
|
+
// Incomplete match with an incomplete content factory available
|
|
59
|
+
result.push(match.matcher.incompleteContentFactory(match.content, request));
|
|
60
|
+
} else {
|
|
61
|
+
// Incomplete match but no incomplete content factory available, use default
|
|
62
|
+
result.push(defaultContentFactory(match.content, request));
|
|
63
|
+
}
|
|
54
64
|
// Update currentIndex to the end of the end of the match
|
|
55
65
|
// And continue with the search after the end of the match
|
|
56
66
|
currentIndex += match.index + match.content.length;
|
|
@@ -60,7 +70,9 @@ export function parseContents(
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
export function findFirstMatch(contentMatchers: ResponseContentMatcher[], text: string): Match | undefined {
|
|
63
|
-
let firstMatch:
|
|
73
|
+
let firstMatch: Match | undefined;
|
|
74
|
+
let firstIncompleteMatch: Match | undefined;
|
|
75
|
+
|
|
64
76
|
for (const matcher of contentMatchers) {
|
|
65
77
|
const startMatch = matcher.start.exec(text);
|
|
66
78
|
if (!startMatch) {
|
|
@@ -70,24 +82,58 @@ export function findFirstMatch(contentMatchers: ResponseContentMatcher[], text:
|
|
|
70
82
|
const endOfStartMatch = startMatch.index + startMatch[0].length;
|
|
71
83
|
if (endOfStartMatch >= text.length) {
|
|
72
84
|
// There is no text after the start match.
|
|
73
|
-
//
|
|
85
|
+
// This is an incomplete match if the matcher has an incompleteContentFactory
|
|
86
|
+
if (matcher.incompleteContentFactory) {
|
|
87
|
+
const incompleteMatch: Match = {
|
|
88
|
+
matcher,
|
|
89
|
+
index: startMatch.index,
|
|
90
|
+
content: text.substring(startMatch.index),
|
|
91
|
+
isComplete: false
|
|
92
|
+
};
|
|
93
|
+
if (!firstIncompleteMatch || incompleteMatch.index < firstIncompleteMatch.index) {
|
|
94
|
+
firstIncompleteMatch = incompleteMatch;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
74
97
|
continue;
|
|
75
98
|
}
|
|
99
|
+
|
|
76
100
|
const remainingTextAfterStartMatch = text.substring(endOfStartMatch);
|
|
77
101
|
const endMatch = matcher.end.exec(remainingTextAfterStartMatch);
|
|
102
|
+
|
|
78
103
|
if (!endMatch) {
|
|
79
|
-
// No end match found,
|
|
104
|
+
// No end match found, this is an incomplete match
|
|
105
|
+
if (matcher.incompleteContentFactory) {
|
|
106
|
+
const incompleteMatch: Match = {
|
|
107
|
+
matcher,
|
|
108
|
+
index: startMatch.index,
|
|
109
|
+
content: text.substring(startMatch.index),
|
|
110
|
+
isComplete: false
|
|
111
|
+
};
|
|
112
|
+
if (!firstIncompleteMatch || incompleteMatch.index < firstIncompleteMatch.index) {
|
|
113
|
+
firstIncompleteMatch = incompleteMatch;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
80
116
|
continue;
|
|
81
117
|
}
|
|
118
|
+
|
|
82
119
|
// Found start and end match.
|
|
83
120
|
// Record the full match, if it is the earliest found so far.
|
|
84
121
|
const index = startMatch.index;
|
|
85
122
|
const contentEnd = index + startMatch[0].length + endMatch.index + endMatch[0].length;
|
|
86
123
|
const content = text.substring(index, contentEnd);
|
|
124
|
+
const completeMatch: Match = { matcher, index, content, isComplete: true };
|
|
125
|
+
|
|
87
126
|
if (!firstMatch || index < firstMatch.index) {
|
|
88
|
-
firstMatch =
|
|
127
|
+
firstMatch = completeMatch;
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
|
-
|
|
130
|
+
|
|
131
|
+
// If we found a complete match, return it
|
|
132
|
+
if (firstMatch) {
|
|
133
|
+
return firstMatch;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Otherwise, return the first incomplete match if one exists
|
|
137
|
+
return firstIncompleteMatch;
|
|
92
138
|
}
|
|
93
139
|
|
|
@@ -23,7 +23,7 @@ import { injectable } from '@theia/core/shared/inversify';
|
|
|
23
23
|
|
|
24
24
|
export type ResponseContentFactory = (content: string, request: MutableChatRequestModel) => ChatResponseContent;
|
|
25
25
|
|
|
26
|
-
export const MarkdownContentFactory: ResponseContentFactory = (content: string) =>
|
|
26
|
+
export const MarkdownContentFactory: ResponseContentFactory = (content: string, request: MutableChatRequestModel) =>
|
|
27
27
|
new MarkdownChatResponseContentImpl(content);
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -53,14 +53,32 @@ export interface ResponseContentMatcher {
|
|
|
53
53
|
* from start index to end index of the match (including delimiters).
|
|
54
54
|
*/
|
|
55
55
|
contentFactory: ResponseContentFactory;
|
|
56
|
+
/**
|
|
57
|
+
* Optional factory for creating a response content when only the start delimiter has been matched,
|
|
58
|
+
* but not yet the end delimiter. Used during streaming to provide better visual feedback.
|
|
59
|
+
* If not provided, the default content factory will be used until the end delimiter is matched.
|
|
60
|
+
*/
|
|
61
|
+
incompleteContentFactory?: ResponseContentFactory;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export const CodeContentMatcher: ResponseContentMatcher = {
|
|
59
|
-
|
|
65
|
+
// Only match when we have the complete first line ending with a newline
|
|
66
|
+
// This ensures we have the full language specification before creating the editor
|
|
67
|
+
start: /^```.*\n/m,
|
|
60
68
|
end: /^```$/m,
|
|
61
|
-
contentFactory: (content: string) => {
|
|
69
|
+
contentFactory: (content: string, request: MutableChatRequestModel) => {
|
|
62
70
|
const language = content.match(/^```(\w+)/)?.[1] || '';
|
|
63
71
|
const code = content.replace(/^```(\w+)\n|```$/g, '');
|
|
72
|
+
return new CodeChatResponseContentImpl(code.trim(), language);
|
|
73
|
+
},
|
|
74
|
+
incompleteContentFactory: (content: string, request: MutableChatRequestModel) => {
|
|
75
|
+
// By this point, we know we have at least the complete first line with ```
|
|
76
|
+
const firstLine = content.split('\n')[0];
|
|
77
|
+
const language = firstLine.match(/^```(\w+)/)?.[1] || '';
|
|
78
|
+
|
|
79
|
+
// Remove the first line to get just the code content
|
|
80
|
+
const code = content.substring(content.indexOf('\n') + 1);
|
|
81
|
+
|
|
64
82
|
return new CodeChatResponseContentImpl(code.trim(), language);
|
|
65
83
|
}
|
|
66
84
|
};
|