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

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.
@@ -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
+ });
@@ -0,0 +1,92 @@
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
+ import { ChatResponseContent } from './chat-model';
17
+ import { CodeContentMatcher, MarkdownContentFactory, ResponseContentFactory, ResponseContentMatcher } from './response-content-matcher';
18
+
19
+ interface Match {
20
+ matcher: ResponseContentMatcher;
21
+ index: number;
22
+ content: string;
23
+ }
24
+
25
+ export function parseContents(
26
+ text: string,
27
+ contentMatchers: ResponseContentMatcher[] = [CodeContentMatcher],
28
+ defaultContentFactory: ResponseContentFactory = MarkdownContentFactory
29
+ ): ChatResponseContent[] {
30
+ const result: ChatResponseContent[] = [];
31
+
32
+ let currentIndex = 0;
33
+ while (currentIndex < text.length) {
34
+ const remainingText = text.substring(currentIndex);
35
+ const match = findFirstMatch(contentMatchers, remainingText);
36
+ if (!match) {
37
+ // Add the remaining text as default content
38
+ if (remainingText.length > 0) {
39
+ result.push(defaultContentFactory(remainingText));
40
+ }
41
+ break;
42
+ }
43
+ // We have a match
44
+ // 1. Add preceding text as default content
45
+ if (match.index > 0) {
46
+ const precedingContent = remainingText.substring(0, match.index);
47
+ if (precedingContent.trim().length > 0) {
48
+ result.push(defaultContentFactory(precedingContent));
49
+ }
50
+ }
51
+ // 2. Add the matched content object
52
+ result.push(match.matcher.contentFactory(match.content));
53
+ // Update currentIndex to the end of the end of the match
54
+ // And continue with the search after the end of the match
55
+ currentIndex += match.index + match.content.length;
56
+ }
57
+
58
+ return result;
59
+ }
60
+
61
+ export function findFirstMatch(contentMatchers: ResponseContentMatcher[], text: string): Match | undefined {
62
+ let firstMatch: { matcher: ResponseContentMatcher, index: number, content: string } | undefined;
63
+ for (const matcher of contentMatchers) {
64
+ const startMatch = matcher.start.exec(text);
65
+ if (!startMatch) {
66
+ // No start match found, try next matcher.
67
+ continue;
68
+ }
69
+ const endOfStartMatch = startMatch.index + startMatch[0].length;
70
+ if (endOfStartMatch >= text.length) {
71
+ // There is no text after the start match.
72
+ // No need to search for the end match yet, try next matcher.
73
+ continue;
74
+ }
75
+ const remainingTextAfterStartMatch = text.substring(endOfStartMatch);
76
+ const endMatch = matcher.end.exec(remainingTextAfterStartMatch);
77
+ if (!endMatch) {
78
+ // No end match found, try next matcher.
79
+ continue;
80
+ }
81
+ // Found start and end match.
82
+ // Record the full match, if it is the earliest found so far.
83
+ const index = startMatch.index;
84
+ const contentEnd = index + startMatch[0].length + endMatch.index + endMatch[0].length;
85
+ const content = text.substring(index, contentEnd);
86
+ if (!firstMatch || index < firstMatch.index) {
87
+ firstMatch = { matcher, index, content };
88
+ }
89
+ }
90
+ return firstMatch;
91
+ }
92
+
@@ -0,0 +1,102 @@
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
+ import {
17
+ ChatResponseContent,
18
+ CodeChatResponseContentImpl,
19
+ MarkdownChatResponseContentImpl
20
+ } from './chat-model';
21
+ import { injectable } from '@theia/core/shared/inversify';
22
+
23
+ export type ResponseContentFactory = (content: string) => ChatResponseContent;
24
+
25
+ export const MarkdownContentFactory: ResponseContentFactory = (content: string) =>
26
+ new MarkdownChatResponseContentImpl(content);
27
+
28
+ /**
29
+ * Default response content factory used if no other `ResponseContentMatcher` applies.
30
+ * By default, this factory creates a markdown content object.
31
+ *
32
+ * @see MarkdownChatResponseContentImpl
33
+ */
34
+ @injectable()
35
+ export class DefaultResponseContentFactory {
36
+ create(content: string): ChatResponseContent {
37
+ return MarkdownContentFactory(content);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Clients can contribute response content matchers to parse a chat response into specific
43
+ * `ChatResponseContent` instances.
44
+ */
45
+ export interface ResponseContentMatcher {
46
+ /** Regular expression for finding the start delimiter. */
47
+ start: RegExp;
48
+ /** Regular expression for finding the start delimiter. */
49
+ end: RegExp;
50
+ /**
51
+ * The factory creating a response content from the matching content,
52
+ * from start index to end index of the match (including delimiters).
53
+ */
54
+ contentFactory: ResponseContentFactory;
55
+ }
56
+
57
+ export const CodeContentMatcher: ResponseContentMatcher = {
58
+ start: /^```.*?$/m,
59
+ end: /^```$/m,
60
+ contentFactory: (content: string) => {
61
+ const language = content.match(/^```(\w+)/)?.[1] || '';
62
+ const code = content.replace(/^```(\w+)\n|```$/g, '');
63
+ return new CodeChatResponseContentImpl(code.trim(), language);
64
+ }
65
+ };
66
+
67
+ /**
68
+ * Clients can contribute response content matchers to parse the response content.
69
+ *
70
+ * The default chat user interface will collect all contributed matchers and use them
71
+ * to parse the response into structured content parts (e.g. code blocks, markdown blocks),
72
+ * which are then rendered with a `ChatResponsePartRenderer` registered for the respective
73
+ * content part type.
74
+ *
75
+ * ### Example
76
+ * ```ts
77
+ * bind(ResponseContentMatcherProvider).to(MyResponseContentMatcherProvider);
78
+ * ...
79
+ * @injectable()
80
+ * export class MyResponseContentMatcherProvider implements ResponseContentMatcherProvider {
81
+ * readonly matchers: ResponseContentMatcher[] = [{
82
+ * start: /^<command>$/m,
83
+ * end: /^</command>$/m,
84
+ * contentFactory: (content: string) => {
85
+ * const command = content.replace(/^<command>\n|<\/command>$/g, '');
86
+ * return new MyChatResponseContentImpl(command.trim());
87
+ * }
88
+ * }];
89
+ * }
90
+ * ```
91
+ *
92
+ * @see ResponseContentMatcher
93
+ */
94
+ export const ResponseContentMatcherProvider = Symbol('ResponseContentMatcherProvider');
95
+ export interface ResponseContentMatcherProvider {
96
+ readonly matchers: ResponseContentMatcher[];
97
+ }
98
+
99
+ @injectable()
100
+ export class DefaultResponseContentMatcherProvider implements ResponseContentMatcherProvider {
101
+ readonly matchers: ResponseContentMatcher[] = [CodeContentMatcher];
102
+ }