@theia/ai-chat 1.54.0 → 1.55.0-next.25

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.
Files changed (34) hide show
  1. package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
  2. package/lib/browser/ai-chat-frontend-module.js +5 -0
  3. package/lib/browser/ai-chat-frontend-module.js.map +1 -1
  4. package/lib/common/chat-agents.d.ts +19 -5
  5. package/lib/common/chat-agents.d.ts.map +1 -1
  6. package/lib/common/chat-agents.js +106 -83
  7. package/lib/common/chat-agents.js.map +1 -1
  8. package/lib/common/chat-model.d.ts +16 -1
  9. package/lib/common/chat-model.d.ts.map +1 -1
  10. package/lib/common/chat-model.js +20 -3
  11. package/lib/common/chat-model.js.map +1 -1
  12. package/lib/common/orchestrator-chat-agent.d.ts.map +1 -1
  13. package/lib/common/orchestrator-chat-agent.js +28 -3
  14. package/lib/common/orchestrator-chat-agent.js.map +1 -1
  15. package/lib/common/parse-contents.d.ts +11 -0
  16. package/lib/common/parse-contents.d.ts.map +1 -0
  17. package/lib/common/parse-contents.js +67 -0
  18. package/lib/common/parse-contents.js.map +1 -0
  19. package/lib/common/parse-contents.spec.d.ts +9 -0
  20. package/lib/common/parse-contents.spec.d.ts.map +1 -0
  21. package/lib/common/parse-contents.spec.js +133 -0
  22. package/lib/common/parse-contents.spec.js.map +1 -0
  23. package/lib/common/response-content-matcher.d.ts +63 -0
  24. package/lib/common/response-content-matcher.d.ts.map +1 -0
  25. package/lib/common/response-content-matcher.js +86 -0
  26. package/lib/common/response-content-matcher.js.map +1 -0
  27. package/package.json +7 -7
  28. package/src/browser/ai-chat-frontend-module.ts +6 -0
  29. package/src/common/chat-agents.ts +108 -90
  30. package/src/common/chat-model.ts +30 -6
  31. package/src/common/orchestrator-chat-agent.ts +29 -4
  32. package/src/common/parse-contents.spec.ts +142 -0
  33. package/src/common/parse-contents.ts +92 -0
  34. package/src/common/response-content-matcher.ts +102 -0
@@ -25,6 +25,7 @@ import {
25
25
  LanguageModel,
26
26
  LanguageModelRequirement,
27
27
  LanguageModelResponse,
28
+ LanguageModelStreamResponse,
28
29
  PromptService,
29
30
  ResolvedPromptTemplate,
30
31
  ToolRequest,
@@ -37,19 +38,20 @@ import {
37
38
  LanguageModelStreamResponsePart,
38
39
  MessageActor,
39
40
  } from '@theia/ai-core/lib/common';
40
- import { CancellationToken, CancellationTokenSource, ILogger, isArray } from '@theia/core';
41
- import { inject, injectable } from '@theia/core/shared/inversify';
41
+ import { CancellationToken, CancellationTokenSource, ContributionProvider, ILogger, isArray } from '@theia/core';
42
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
42
43
  import { ChatAgentService } from './chat-agent-service';
43
44
  import {
44
45
  ChatModel,
45
46
  ChatRequestModel,
46
47
  ChatRequestModelImpl,
47
48
  ChatResponseContent,
48
- CodeChatResponseContentImpl,
49
49
  ErrorChatResponseContentImpl,
50
50
  MarkdownChatResponseContentImpl,
51
51
  ToolCallChatResponseContentImpl
52
52
  } from './chat-model';
53
+ import { findFirstMatch, parseContents } from './parse-contents';
54
+ import { DefaultResponseContentFactory, ResponseContentMatcher, ResponseContentMatcherProvider } from './response-content-matcher';
53
55
 
54
56
  /**
55
57
  * A conversation consists of a sequence of ChatMessages.
@@ -121,13 +123,27 @@ export abstract class AbstractChatAgent {
121
123
  @inject(ILogger) protected logger: ILogger;
122
124
  @inject(CommunicationRecordingService) protected recordingService: CommunicationRecordingService;
123
125
  @inject(PromptService) protected promptService: PromptService;
126
+
127
+ @inject(ContributionProvider) @named(ResponseContentMatcherProvider)
128
+ protected contentMatcherProviders: ContributionProvider<ResponseContentMatcherProvider>;
129
+ protected contentMatchers: ResponseContentMatcher[] = [];
130
+
131
+ @inject(DefaultResponseContentFactory)
132
+ protected defaultContentFactory: DefaultResponseContentFactory;
133
+
124
134
  constructor(
125
135
  public id: string,
126
136
  public languageModelRequirements: LanguageModelRequirement[],
127
137
  protected defaultLanguageModelPurpose: string,
128
138
  public iconClass: string = 'codicon codicon-copilot',
129
139
  public locations: ChatAgentLocation[] = ChatAgentLocation.ALL,
130
- public tags: String[] = ['Chat']) {
140
+ public tags: String[] = ['Chat'],
141
+ public defaultLogging: boolean = true) {
142
+ }
143
+
144
+ @postConstruct()
145
+ init(): void {
146
+ this.contentMatchers = this.contentMatcherProviders.getContributions().flatMap(provider => provider.matchers);
131
147
  }
132
148
 
133
149
  async invoke(request: ChatRequestModelImpl): Promise<void> {
@@ -137,14 +153,16 @@ export abstract class AbstractChatAgent {
137
153
  throw new Error('Couldn\'t find a matching language model. Please check your setup!');
138
154
  }
139
155
  const messages = await this.getMessages(request.session);
140
- this.recordingService.recordRequest({
141
- agentId: this.id,
142
- sessionId: request.session.id,
143
- timestamp: Date.now(),
144
- requestId: request.id,
145
- request: request.request.text,
146
- messages
147
- });
156
+ if (this.defaultLogging) {
157
+ this.recordingService.recordRequest({
158
+ agentId: this.id,
159
+ sessionId: request.session.id,
160
+ timestamp: Date.now(),
161
+ requestId: request.id,
162
+ request: request.request.text,
163
+ messages
164
+ });
165
+ }
148
166
 
149
167
  const systemMessageDescription = await this.getSystemMessageDescription();
150
168
  const tools: Map<string, ToolRequest> = new Map();
@@ -177,18 +195,28 @@ export abstract class AbstractChatAgent {
177
195
  );
178
196
  await this.addContentsToResponse(languageModelResponse, request);
179
197
  request.response.complete();
180
- this.recordingService.recordResponse({
181
- agentId: this.id,
182
- sessionId: request.session.id,
183
- timestamp: Date.now(),
184
- requestId: request.response.requestId,
185
- response: request.response.response.asString()
186
- });
198
+ if (this.defaultLogging) {
199
+ this.recordingService.recordResponse({
200
+ agentId: this.id,
201
+ sessionId: request.session.id,
202
+ timestamp: Date.now(),
203
+ requestId: request.response.requestId,
204
+ response: request.response.response.asString()
205
+ });
206
+ }
187
207
  } catch (e) {
188
208
  this.handleError(request, e);
189
209
  }
190
210
  }
191
211
 
212
+ protected parseContents(text: string): ChatResponseContent[] {
213
+ return parseContents(
214
+ text,
215
+ this.contentMatchers,
216
+ this.defaultContentFactory?.create.bind(this.defaultContentFactory)
217
+ );
218
+ };
219
+
192
220
  protected handleError(request: ChatRequestModelImpl, error: Error): void {
193
221
  request.response.response.addContent(new ErrorChatResponseContentImpl(error));
194
222
  request.response.error(error);
@@ -251,13 +279,22 @@ export abstract class AbstractChatAgent {
251
279
  tools: ToolRequest[] | undefined,
252
280
  token: CancellationToken
253
281
  ): Promise<LanguageModelResponse> {
282
+ const settings = this.getLlmSettings();
254
283
  const languageModelResponse = languageModel.request({
255
284
  messages,
256
285
  tools,
286
+ settings,
257
287
  }, token);
258
288
  return languageModelResponse;
259
289
  }
260
290
 
291
+ /**
292
+ * @returns the settings, such as `temperature`, to be used in all language model requests. Returns `undefined` by default.
293
+ */
294
+ protected getLlmSettings(): { [key: string]: unknown; } | undefined {
295
+ return undefined;
296
+ }
297
+
261
298
  protected abstract addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void>;
262
299
  }
263
300
 
@@ -281,79 +318,33 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
281
318
 
282
319
  protected override async addContentsToResponse(languageModelResponse: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void> {
283
320
  if (isLanguageModelTextResponse(languageModelResponse)) {
284
- request.response.response.addContent(
285
- new MarkdownChatResponseContentImpl(languageModelResponse.text)
286
- );
321
+ const contents = this.parseContents(languageModelResponse.text);
322
+ request.response.response.addContents(contents);
287
323
  request.response.complete();
288
- this.recordingService.recordResponse({
289
- agentId: this.id,
290
- sessionId: request.session.id,
291
- timestamp: Date.now(),
292
- requestId: request.response.requestId,
293
- response: request.response.response.asString()
294
- });
324
+ if (this.defaultLogging) {
325
+ this.recordingService.recordResponse({
326
+ agentId: this.id,
327
+ sessionId: request.session.id,
328
+ timestamp: Date.now(),
329
+ requestId: request.response.requestId,
330
+ response: request.response.response.asString()
331
+
332
+ });
333
+ }
295
334
  return;
296
335
  }
297
336
  if (isLanguageModelStreamResponse(languageModelResponse)) {
298
- for await (const token of languageModelResponse.stream) {
299
- const newContents = this.parse(token, request.response.response.content);
300
- if (isArray(newContents)) {
301
- newContents.forEach(newContent => request.response.response.addContent(newContent));
302
- } else {
303
- request.response.response.addContent(newContents);
304
- }
305
-
306
- const lastContent = request.response.response.content.pop();
307
- if (lastContent === undefined) {
308
- return;
309
- }
310
- const text = lastContent.asString?.();
311
- if (text === undefined) {
312
- return;
313
- }
314
- let curSearchIndex = 0;
315
- const result: ChatResponseContent[] = [];
316
- while (curSearchIndex < text.length) {
317
- // find start of code block: ```[language]\n<code>[\n]```
318
- const codeStartIndex = text.indexOf('```', curSearchIndex);
319
- if (codeStartIndex === -1) {
320
- break;
321
- }
322
-
323
- // find language specifier if present
324
- const newLineIndex = text.indexOf('\n', codeStartIndex + 3);
325
- const language = codeStartIndex + 3 < newLineIndex ? text.substring(codeStartIndex + 3, newLineIndex) : undefined;
326
-
327
- // find end of code block
328
- const codeEndIndex = text.indexOf('```', codeStartIndex + 3);
329
- if (codeEndIndex === -1) {
330
- break;
331
- }
332
-
333
- // add text before code block as markdown content
334
- result.push(new MarkdownChatResponseContentImpl(text.substring(curSearchIndex, codeStartIndex)));
335
- // add code block as code content
336
- const codeText = text.substring(newLineIndex + 1, codeEndIndex).trimEnd();
337
- result.push(new CodeChatResponseContentImpl(codeText, language));
338
- curSearchIndex = codeEndIndex + 3;
339
- }
340
-
341
- if (result.length > 0) {
342
- result.forEach(r => {
343
- request.response.response.addContent(r);
344
- });
345
- } else {
346
- request.response.response.addContent(lastContent);
347
- }
348
- }
337
+ await this.addStreamResponse(languageModelResponse, request);
349
338
  request.response.complete();
350
- this.recordingService.recordResponse({
351
- agentId: this.id,
352
- sessionId: request.session.id,
353
- timestamp: Date.now(),
354
- requestId: request.response.requestId,
355
- response: request.response.response.asString()
356
- });
339
+ if (this.defaultLogging) {
340
+ this.recordingService.recordResponse({
341
+ agentId: this.id,
342
+ sessionId: request.session.id,
343
+ timestamp: Date.now(),
344
+ requestId: request.response.requestId,
345
+ response: request.response.response.asString()
346
+ });
347
+ }
357
348
  return;
358
349
  }
359
350
  this.logger.error(
@@ -366,11 +357,38 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
366
357
  );
367
358
  }
368
359
 
369
- private parse(token: LanguageModelStreamResponsePart, previousContent: ChatResponseContent[]): ChatResponseContent | ChatResponseContent[] {
360
+ protected async addStreamResponse(languageModelResponse: LanguageModelStreamResponse, request: ChatRequestModelImpl): Promise<void> {
361
+ for await (const token of languageModelResponse.stream) {
362
+ const newContents = this.parse(token, request.response.response.content);
363
+ if (isArray(newContents)) {
364
+ request.response.response.addContents(newContents);
365
+ } else {
366
+ request.response.response.addContent(newContents);
367
+ }
368
+
369
+ const lastContent = request.response.response.content.pop();
370
+ if (lastContent === undefined) {
371
+ return;
372
+ }
373
+ const text = lastContent.asString?.();
374
+ if (text === undefined) {
375
+ return;
376
+ }
377
+
378
+ const result: ChatResponseContent[] = findFirstMatch(this.contentMatchers, text) ? this.parseContents(text) : [];
379
+ if (result.length > 0) {
380
+ request.response.response.addContents(result);
381
+ } else {
382
+ request.response.response.addContent(lastContent);
383
+ }
384
+ }
385
+ }
386
+
387
+ protected parse(token: LanguageModelStreamResponsePart, previousContent: ChatResponseContent[]): ChatResponseContent | ChatResponseContent[] {
370
388
  const content = token.content;
371
389
  // eslint-disable-next-line no-null/no-null
372
390
  if (content !== undefined && content !== null) {
373
- return new MarkdownChatResponseContentImpl(content);
391
+ return this.defaultContentFactory.create(content);
374
392
  }
375
393
  const toolCalls = token.tool_calls;
376
394
  if (toolCalls !== undefined) {
@@ -378,7 +396,7 @@ export abstract class AbstractStreamParsingChatAgent extends AbstractChatAgent {
378
396
  new ToolCallChatResponseContentImpl(toolCall.id, toolCall.function?.name, toolCall.function?.arguments, toolCall.finished, toolCall.result));
379
397
  return toolCallContents;
380
398
  }
381
- return new MarkdownChatResponseContentImpl('');
399
+ return this.defaultContentFactory.create('');
382
400
  }
383
401
 
384
402
  }
@@ -73,6 +73,7 @@ export interface ChatRequestModel {
73
73
  readonly response: ChatResponseModel;
74
74
  readonly message: ParsedChatRequest;
75
75
  readonly agentId?: string;
76
+ readonly data?: { [key: string]: unknown };
76
77
  }
77
78
 
78
79
  export interface ChatProgressMessage {
@@ -342,14 +343,29 @@ export class ChatRequestModelImpl implements ChatRequestModel {
342
343
  protected _request: ChatRequest;
343
344
  protected _response: ChatResponseModelImpl;
344
345
  protected _agentId?: string;
346
+ protected _data: { [key: string]: unknown };
345
347
 
346
- constructor(session: ChatModel, public readonly message: ParsedChatRequest, agentId?: string) {
348
+ constructor(session: ChatModel, public readonly message: ParsedChatRequest, agentId?: string,
349
+ data: { [key: string]: unknown } = {}) {
347
350
  // TODO accept serialized data as a parameter to restore a previously saved ChatRequestModel
348
351
  this._request = message.request;
349
352
  this._id = generateUuid();
350
353
  this._session = session;
351
354
  this._response = new ChatResponseModelImpl(this._id, agentId);
352
355
  this._agentId = agentId;
356
+ this._data = data;
357
+ }
358
+
359
+ get data(): { [key: string]: unknown } | undefined {
360
+ return this._data;
361
+ }
362
+
363
+ addData(key: string, value: unknown): void {
364
+ this._data[key] = value;
365
+ }
366
+
367
+ getDataByKey(key: string): unknown {
368
+ return this._data[key];
353
369
  }
354
370
 
355
371
  get id(): string {
@@ -601,10 +617,20 @@ class ChatResponseImpl implements ChatResponse {
601
617
  return this._content;
602
618
  }
603
619
 
620
+ addContents(contents: ChatResponseContent[]): void {
621
+ contents.forEach(c => this.doAddContent(c));
622
+ this._onDidChangeEmitter.fire();
623
+ }
624
+
604
625
  addContent(nextContent: ChatResponseContent): void {
605
626
  // TODO: Support more complex merges affecting different content than the last, e.g. via some kind of ProcessorRegistry
606
627
  // TODO: Support more of the built-in VS Code behavior, see
607
628
  // https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts#L188-L244
629
+ this.doAddContent(nextContent);
630
+ this._onDidChangeEmitter.fire();
631
+ }
632
+
633
+ protected doAddContent(nextContent: ChatResponseContent): void {
608
634
  if (ToolCallChatResponseContent.is(nextContent) && nextContent.id !== undefined) {
609
635
  const fittingTool = this._content.find(c => ToolCallChatResponseContent.is(c) && c.id === nextContent.id);
610
636
  if (fittingTool !== undefined) {
@@ -613,10 +639,9 @@ class ChatResponseImpl implements ChatResponse {
613
639
  this._content.push(nextContent);
614
640
  }
615
641
  } else {
616
- const lastElement =
617
- this._content.length > 0
618
- ? this._content[this._content.length - 1]
619
- : undefined;
642
+ const lastElement = this._content.length > 0
643
+ ? this._content[this._content.length - 1]
644
+ : undefined;
620
645
  if (lastElement?.kind === nextContent.kind && ChatResponseContent.hasMerge(lastElement)) {
621
646
  const mergeSuccess = lastElement.merge(nextContent);
622
647
  if (!mergeSuccess) {
@@ -627,7 +652,6 @@ class ChatResponseImpl implements ChatResponse {
627
652
  }
628
653
  }
629
654
  this._updateResponseRepresentation();
630
- this._onDidChangeEmitter.fire();
631
655
  }
632
656
 
633
657
  protected _updateResponseRepresentation(): void {
@@ -14,7 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { AgentSpecificVariables, getJsonOfResponse, LanguageModelResponse } from '@theia/ai-core';
17
+ import { AgentSpecificVariables, getJsonOfText, getTextOfResponse, LanguageModelResponse } from '@theia/ai-core';
18
18
  import {
19
19
  PromptTemplate
20
20
  } from '@theia/ai-core/lib/common';
@@ -22,6 +22,7 @@ import { inject, injectable } from '@theia/core/shared/inversify';
22
22
  import { ChatAgentService } from './chat-agent-service';
23
23
  import { AbstractStreamParsingChatAgent, ChatAgent, SystemMessageDescription } from './chat-agents';
24
24
  import { ChatRequestModelImpl, InformationalChatResponseContentImpl } from './chat-model';
25
+ import { generateUuid } from '@theia/core';
25
26
 
26
27
  export const orchestratorTemplate: PromptTemplate = {
27
28
  id: 'orchestrator-system',
@@ -59,6 +60,7 @@ You must only use the \`id\` attribute of the agent, never the name.
59
60
  `};
60
61
 
61
62
  export const OrchestratorChatAgentId = 'Orchestrator';
63
+ const OrchestatorRequestIdKey = 'orchestatorRequestIdKey';
62
64
 
63
65
  @injectable()
64
66
  export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implements ChatAgent {
@@ -74,7 +76,7 @@ export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implem
74
76
  super(OrchestratorChatAgentId, [{
75
77
  purpose: 'agent-selection',
76
78
  identifier: 'openai/gpt-4o',
77
- }], 'agent-selection', 'codicon codicon-symbol-boolean');
79
+ }], 'agent-selection', 'codicon codicon-symbol-boolean', undefined, undefined, false);
78
80
  this.name = OrchestratorChatAgentId;
79
81
  this.description = 'This agent analyzes the user request against the description of all available chat agents and selects the best fitting agent to answer the request \
80
82
  (by using AI).The user\'s request will be directly delegated to the selected agent without further confirmation.';
@@ -88,8 +90,19 @@ export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implem
88
90
  @inject(ChatAgentService)
89
91
  protected chatAgentService: ChatAgentService;
90
92
 
91
- override invoke(request: ChatRequestModelImpl): Promise<void> {
93
+ override async invoke(request: ChatRequestModelImpl): Promise<void> {
92
94
  request.response.addProgressMessage({ content: 'Determining the most appropriate agent', status: 'inProgress' });
95
+ // We generate a dedicated ID for recording the orchestrator request/response, as we will forward the original request to another agent
96
+ const orchestartorRequestId = generateUuid();
97
+ request.addData(OrchestatorRequestIdKey, orchestartorRequestId);
98
+ const userPrompt = request.request.text;
99
+ this.recordingService.recordRequest({
100
+ agentId: this.id,
101
+ sessionId: request.session.id,
102
+ timestamp: Date.now(),
103
+ requestId: orchestartorRequestId,
104
+ request: userPrompt,
105
+ });
93
106
  return super.invoke(request);
94
107
  }
95
108
 
@@ -100,8 +113,20 @@ export class OrchestratorChatAgent extends AbstractStreamParsingChatAgent implem
100
113
 
101
114
  protected override async addContentsToResponse(response: LanguageModelResponse, request: ChatRequestModelImpl): Promise<void> {
102
115
  let agentIds: string[] = [];
116
+ const responseText = await getTextOfResponse(response);
117
+ // We use the previously generated, dedicated ID to log the orchestrator response before we forward the original request
118
+ const orchestratorRequestId = request.getDataByKey(OrchestatorRequestIdKey);
119
+ if (typeof orchestratorRequestId === 'string') {
120
+ this.recordingService.recordResponse({
121
+ agentId: this.id,
122
+ sessionId: request.session.id,
123
+ timestamp: Date.now(),
124
+ requestId: orchestratorRequestId,
125
+ response: responseText,
126
+ });
127
+ }
103
128
  try {
104
- const jsonResponse = await getJsonOfResponse(response);
129
+ const jsonResponse = await getJsonOfText(responseText);
105
130
  if (Array.isArray(jsonResponse)) {
106
131
  agentIds = jsonResponse.filter((id: string) => id !== this.id);
107
132
  }
@@ -0,0 +1,142 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { ChatResponseContent, CodeChatResponseContentImpl, MarkdownChatResponseContentImpl } from './chat-model';
19
+ import { parseContents } from './parse-contents';
20
+ import { CodeContentMatcher, ResponseContentMatcher } from './response-content-matcher';
21
+
22
+ export class CommandChatResponseContentImpl implements ChatResponseContent {
23
+ constructor(public readonly command: string) { }
24
+ kind = 'command';
25
+ }
26
+
27
+ export const CommandContentMatcher: ResponseContentMatcher = {
28
+ start: /^<command>$/m,
29
+ end: /^<\/command>$/m,
30
+ contentFactory: (content: string) => {
31
+ const code = content.replace(/^<command>\n|<\/command>$/g, '');
32
+ return new CommandChatResponseContentImpl(code.trim());
33
+ }
34
+ };
35
+
36
+ describe('parseContents', () => {
37
+ it('should parse code content', () => {
38
+ const text = '```typescript\nconsole.log("Hello World");\n```';
39
+ const result = parseContents(text);
40
+ expect(result).to.deep.equal([new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript')]);
41
+ });
42
+
43
+ it('should parse markdown content', () => {
44
+ const text = 'Hello **World**';
45
+ const result = parseContents(text);
46
+ expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Hello **World**')]);
47
+ });
48
+
49
+ it('should parse multiple content blocks', () => {
50
+ const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**';
51
+ const result = parseContents(text);
52
+ expect(result).to.deep.equal([
53
+ new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
54
+ new MarkdownChatResponseContentImpl('\nHello **World**')
55
+ ]);
56
+ });
57
+
58
+ it('should parse multiple content blocks with different languages', () => {
59
+ const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")\n```';
60
+ const result = parseContents(text);
61
+ expect(result).to.deep.equal([
62
+ new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
63
+ new CodeChatResponseContentImpl('print("Hello World")', 'python')
64
+ ]);
65
+ });
66
+
67
+ it('should parse multiple content blocks with different languages and markdown', () => {
68
+ const text = '```typescript\nconsole.log("Hello World");\n```\nHello **World**\n```python\nprint("Hello World")\n```';
69
+ const result = parseContents(text);
70
+ expect(result).to.deep.equal([
71
+ new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
72
+ new MarkdownChatResponseContentImpl('\nHello **World**\n'),
73
+ new CodeChatResponseContentImpl('print("Hello World")', 'python')
74
+ ]);
75
+ });
76
+
77
+ it('should parse content blocks with empty content', () => {
78
+ const text = '```typescript\n```\nHello **World**\n```python\nprint("Hello World")\n```';
79
+ const result = parseContents(text);
80
+ expect(result).to.deep.equal([
81
+ new CodeChatResponseContentImpl('', 'typescript'),
82
+ new MarkdownChatResponseContentImpl('\nHello **World**\n'),
83
+ new CodeChatResponseContentImpl('print("Hello World")', 'python')
84
+ ]);
85
+ });
86
+
87
+ it('should parse content with markdown, code, and markdown', () => {
88
+ const text = 'Hello **World**\n```typescript\nconsole.log("Hello World");\n```\nGoodbye **World**';
89
+ const result = parseContents(text);
90
+ expect(result).to.deep.equal([
91
+ new MarkdownChatResponseContentImpl('Hello **World**\n'),
92
+ new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
93
+ new MarkdownChatResponseContentImpl('\nGoodbye **World**')
94
+ ]);
95
+ });
96
+
97
+ it('should handle text with no special content', () => {
98
+ const text = 'Just some plain text.';
99
+ const result = parseContents(text);
100
+ expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('Just some plain text.')]);
101
+ });
102
+
103
+ it('should handle text with only start code block', () => {
104
+ const text = '```typescript\nconsole.log("Hello World");';
105
+ const result = parseContents(text);
106
+ expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('```typescript\nconsole.log("Hello World");')]);
107
+ });
108
+
109
+ it('should handle text with only end code block', () => {
110
+ const text = 'console.log("Hello World");\n```';
111
+ const result = parseContents(text);
112
+ expect(result).to.deep.equal([new MarkdownChatResponseContentImpl('console.log("Hello World");\n```')]);
113
+ });
114
+
115
+ it('should handle text with unmatched code block', () => {
116
+ const text = '```typescript\nconsole.log("Hello World");\n```\n```python\nprint("Hello World")';
117
+ const result = parseContents(text);
118
+ expect(result).to.deep.equal([
119
+ new CodeChatResponseContentImpl('console.log("Hello World");', 'typescript'),
120
+ new MarkdownChatResponseContentImpl('\n```python\nprint("Hello World")')
121
+ ]);
122
+ });
123
+
124
+ it('should parse code block without newline after language', () => {
125
+ const text = '```typescript console.log("Hello World");```';
126
+ const result = parseContents(text);
127
+ expect(result).to.deep.equal([
128
+ new MarkdownChatResponseContentImpl('```typescript console.log("Hello World");```')
129
+ ]);
130
+ });
131
+
132
+ it('should parse with matches of multiple different matchers and default', () => {
133
+ const text = '<command>\nMY_SPECIAL_COMMAND\n</command>\nHello **World**\n```python\nprint("Hello World")\n```\n<command>\nMY_SPECIAL_COMMAND2\n</command>';
134
+ const result = parseContents(text, [CodeContentMatcher, CommandContentMatcher]);
135
+ expect(result).to.deep.equal([
136
+ new CommandChatResponseContentImpl('MY_SPECIAL_COMMAND'),
137
+ new MarkdownChatResponseContentImpl('\nHello **World**\n'),
138
+ new CodeChatResponseContentImpl('print("Hello World")', 'python'),
139
+ new CommandChatResponseContentImpl('MY_SPECIAL_COMMAND2'),
140
+ ]);
141
+ });
142
+ });