Skip to content

Commit b88b186

Browse files
authored
AIIntegration: Add TextTransform commands and prompts
1 parent ca9bf68 commit b88b186

28 files changed

+1663
-218
lines changed

packages/devextreme/js/__internal/core/ai_integration/commands/base.test.ts

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ describe('BaseCommand', () => {
6262
});
6363

6464
describe('constructor', () => {
65-
it('stores PromptManager and RequestManager correctly', () => {
65+
it('should store PromptManager and RequestManager instances', () => {
6666
// @ts-expect-error Access to protected property for a test
6767
expect(command.promptManager).toBe(promptManager);
6868
// @ts-expect-error Access to protected property for a test
@@ -71,7 +71,7 @@ describe('BaseCommand', () => {
7171
});
7272

7373
describe('execute', () => {
74-
it('getTemplateName returns value correctly', () => {
74+
it('getTemplateName should return the name of the corresponding template', () => {
7575
const spy = jest.spyOn(command, 'getTemplateName');
7676

7777
command.execute(params, {});
@@ -80,7 +80,7 @@ describe('BaseCommand', () => {
8080
expect(spy).toHaveReturnedWith('test-template-name');
8181
});
8282

83-
it('buildPromptData receives and returns correct data', () => {
83+
it('buildPromptData should receive and returns correct data', () => {
8484
const spy = jest.spyOn(command, 'buildPromptData');
8585

8686
command.execute(params, {});
@@ -93,7 +93,7 @@ describe('BaseCommand', () => {
9393
});
9494
});
9595

96-
it('parseResult receives correct value and returns expected result', async () => {
96+
it('parseResult should receive correct value and return expected result', async () => {
9797
const spy = jest.spyOn(command, 'parseResult');
9898

9999
command.execute(params, {});
@@ -105,14 +105,14 @@ describe('BaseCommand', () => {
105105
expect(spy).toHaveReturnedWith('Parsed result: AI response');
106106
});
107107

108-
it('callbacks are called correctly', async () => {
109-
const callbacks = {
108+
it('callbacks should be called a specified number of times', async () => {
109+
const callbacks: RequestCallbacks<unknown> = {
110110
onComplete: jest.fn(),
111111
onError: jest.fn(),
112112
onChunk: jest.fn(),
113113
};
114114

115-
command.execute(params, callbacks as RequestCallbacks);
115+
command.execute(params, callbacks);
116116

117117
await new Promise(process.nextTick);
118118

@@ -121,44 +121,46 @@ describe('BaseCommand', () => {
121121
expect(callbacks.onChunk).toHaveBeenCalledTimes(2);
122122
});
123123

124-
it('onComplete is called with parseResult output', async () => {
125-
const callbacks = { onComplete: jest.fn() };
124+
it('onComplete should be called with parseResult output', async () => {
125+
const callbacks: RequestCallbacks<unknown> = { onComplete: jest.fn() };
126126

127-
command.execute(params, callbacks as RequestCallbacks);
127+
command.execute(params, callbacks);
128128

129129
await new Promise(process.nextTick);
130130

131131
expect(callbacks.onComplete).toHaveBeenCalledWith('Parsed result: AI response');
132132
});
133133

134-
it('calls onError if request fails', async () => {
135-
const originalSendRequest = requestManager.sendRequest;
136-
137-
requestManager.sendRequest = (_, callbacks) => {
138-
callbacks.onError?.(new Error('Test error'));
134+
describe('if request fails', () => {
135+
it('should call onError ', async () => {
136+
const originalSendRequest = requestManager.sendRequest;
139137

140-
return (): void => {};
141-
};
138+
requestManager.sendRequest = (_, callbacks) => {
139+
callbacks.onError?.(new Error('Test error'));
142140

143-
try {
144-
const callbacks = {
145-
onError: jest.fn(),
146-
onComplete: jest.fn(),
141+
return (): void => {};
147142
};
148143

149-
command.execute(params, callbacks as RequestCallbacks);
144+
try {
145+
const callbacks: RequestCallbacks<unknown> = {
146+
onError: jest.fn(),
147+
onComplete: jest.fn(),
148+
};
150149

151-
await new Promise(process.nextTick);
150+
command.execute(params, callbacks);
152151

153-
expect(callbacks.onError).toHaveBeenCalledTimes(1);
154-
expect(callbacks.onError).toHaveBeenCalledWith(new Error('Test error'));
155-
expect(callbacks.onComplete).toHaveBeenCalledTimes(0);
156-
} finally {
157-
requestManager.sendRequest = originalSendRequest;
158-
}
152+
await new Promise(process.nextTick);
153+
154+
expect(callbacks.onError).toHaveBeenCalledTimes(1);
155+
expect(callbacks.onError).toHaveBeenCalledWith(new Error('Test error'));
156+
expect(callbacks.onComplete).toHaveBeenCalledTimes(0);
157+
} finally {
158+
requestManager.sendRequest = originalSendRequest;
159+
}
160+
});
159161
});
160162

161-
it('calls onChunk for each chunk and onComplete correctly', () => {
163+
it('should call onChunk for each chunk and onComplete a specified number of times with expected params', () => {
162164
const originalSendRequest = requestManager.sendRequest;
163165

164166
requestManager.sendRequest = (_, callbacks) => {
@@ -185,7 +187,7 @@ describe('BaseCommand', () => {
185187
}
186188
});
187189

188-
it('executes with undefined params without errors', async () => {
190+
it('should execute with undefined params without errors', async () => {
189191
const sendRequestSpy = jest.spyOn(requestManager, 'sendRequest');
190192
const onError = jest.fn();
191193

@@ -197,7 +199,7 @@ describe('BaseCommand', () => {
197199
expect(sendRequestSpy).toHaveBeenCalledTimes(1);
198200
});
199201

200-
it('executes with partial callbacks without errors', async () => {
202+
it('should execute with partial callbacks without errors', async () => {
201203
const sendRequestSpy = jest.spyOn(requestManager, 'sendRequest');
202204
const callbacks = { onChunk: jest.fn() };
203205

@@ -209,12 +211,12 @@ describe('BaseCommand', () => {
209211
expect(sendRequestSpy).toHaveBeenCalledTimes(1);
210212
});
211213

212-
it('executes with undefined callbacks without errors', () => {
214+
it('should execute with undefined callbacks without errors', () => {
213215
const sendRequestSpy = jest.spyOn(requestManager, 'sendRequest');
214216

215217
expect(command.execute(
216218
params,
217-
(undefined as unknown as RequestCallbacks),
219+
(undefined as unknown as RequestCallbacks<unknown>),
218220
)).not.toThrow();
219221

220222
expect(sendRequestSpy).toHaveBeenCalledTimes(1);

packages/devextreme/js/__internal/core/ai_integration/commands/base.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
1-
import type { BaseCommandResult, RequestCallbacks } from '@js/common/ai-integration';
1+
import type { RequestCallbacks } from '@js/common/ai-integration';
22
import type { PromptData, PromptManager, PromptTemplateName } from '@ts/core/ai_integration/core/prompt_manager';
3-
import type { RequestManager } from '@ts/core/ai_integration/core/request_manager';
3+
import type { RequestManager, RequestManagerCallbacks } from '@ts/core/ai_integration/core/request_manager';
44

5-
export abstract class BaseCommand<TParams, TResult extends BaseCommandResult> {
5+
export abstract class BaseCommand<TParams, TResult> {
66
constructor(
77
protected promptManager: PromptManager,
88
protected requestManager: RequestManager,
99
) {}
1010

11-
public execute(params: TParams, callbacks: RequestCallbacks): () => void {
11+
public execute(params: TParams, callbacks: RequestCallbacks<TResult>): () => void {
1212
const templateName = this.getTemplateName();
1313
const data = this.buildPromptData(params);
1414

1515
const prompt = this.promptManager.buildPrompt(templateName, data);
1616

17-
const abort = this.requestManager.sendRequest(prompt, {
17+
const requestManagerCallbacks: RequestManagerCallbacks = {
1818
onChunk: (chunk) => { callbacks?.onChunk?.(chunk); },
1919
onComplete: (result) => {
2020
const finalResponse = this.parseResult(result);
2121

2222
callbacks?.onComplete?.(finalResponse);
2323
},
2424
onError: (error) => { callbacks?.onError?.(error); },
25-
});
25+
};
26+
27+
const abort = this.requestManager.sendRequest(prompt, requestManagerCallbacks);
2628

2729
return abort;
2830
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import {
2+
beforeEach,
3+
describe,
4+
expect,
5+
it,
6+
jest,
7+
} from '@jest/globals';
8+
import type {
9+
AIProvider,
10+
ChangeStyleCommandParams,
11+
ChangeStyleCommandResult,
12+
RequestCallbacks,
13+
} from '@js/common/ai-integration';
14+
import { ChangeStyleCommand } from '@ts/core/ai_integration/commands';
15+
import type { PromptData } from '@ts/core/ai_integration/core/prompt_manager';
16+
import { PromptManager } from '@ts/core/ai_integration/core/prompt_manager';
17+
import { RequestManager } from '@ts/core/ai_integration/core/request_manager';
18+
import { Provider } from '@ts/core/ai_integration/test_utils/provider_mock';
19+
20+
describe('ChangeStyleCommand', () => {
21+
const params: ChangeStyleCommandParams = {
22+
text: 'text to style change',
23+
writingStyle: 'creative',
24+
};
25+
26+
let promptManager = null as unknown as PromptManager;
27+
let requestManager = null as unknown as RequestManager;
28+
let command = null as unknown as ChangeStyleCommand;
29+
30+
beforeEach(() => {
31+
const provider: AIProvider = new Provider();
32+
33+
requestManager = new RequestManager(provider);
34+
promptManager = new PromptManager();
35+
36+
command = new ChangeStyleCommand(promptManager, requestManager);
37+
});
38+
39+
describe('getTemplateName', () => {
40+
it('should return the name of the corresponding template', () => {
41+
// @ts-expect-error Access to protected property for a test
42+
const templateName = command.getTemplateName();
43+
44+
expect(templateName).toBe('changeStyle');
45+
});
46+
});
47+
48+
describe('buildPromptData', () => {
49+
it('should form PromptData with empty object', () => {
50+
// @ts-expect-error Access to protected property for a test
51+
const promptData: PromptData = command.buildPromptData(params);
52+
53+
expect(promptData).toEqual({
54+
system: { writingStyle: params.writingStyle },
55+
user: { text: params.text },
56+
});
57+
});
58+
});
59+
60+
describe('parseResult', () => {
61+
it('should return the string without changes', () => {
62+
const response = 'Shorten text';
63+
// @ts-expect-error Access to protected property for a test
64+
const result = command.parseResult(response);
65+
66+
expect(result).toBe(response);
67+
});
68+
});
69+
70+
describe('execute', () => {
71+
const callbacks: RequestCallbacks<ChangeStyleCommandResult> = { onComplete: () => {} };
72+
73+
it('promptManager.buildPrompt should be called with parameters containing the passed values', () => {
74+
const buildPromptSpy = jest.spyOn(promptManager, 'buildPrompt');
75+
76+
command.execute(params, callbacks);
77+
78+
expect(buildPromptSpy).toHaveBeenCalledTimes(1);
79+
expect(promptManager.buildPrompt).toHaveBeenCalledWith('changeStyle', {
80+
system: { writingStyle: params.writingStyle },
81+
user: { text: params.text },
82+
});
83+
});
84+
85+
it('promptManager.buildPrompt should should return prompt with passed values', () => {
86+
jest.spyOn(promptManager, 'buildPrompt');
87+
88+
command.execute(params, callbacks);
89+
90+
expect(promptManager.buildPrompt).toHaveReturnedWith({
91+
system: 'Rewrite the text provided to match the creative writing style. Ensure the rewritten text follows the grammatical rules and stylistic conventions of the specified style. Preserve the original meaning and context. Use complete sentences and a professional tone. Return answer with no markdown formatting.',
92+
user: params.text,
93+
});
94+
});
95+
96+
it('should call provider.sendRequest once and return the abort function', () => {
97+
const sendRequestSpy = jest.spyOn(requestManager, 'sendRequest');
98+
99+
const abort = command.execute(params, callbacks);
100+
101+
expect(typeof abort).toBe('function');
102+
expect(sendRequestSpy).toHaveBeenCalledTimes(1);
103+
});
104+
});
105+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { ChangeStyleCommandParams, ChangeStyleCommandResult } from '@js/common/ai-integration';
2+
import { BaseCommand } from '@ts/core/ai_integration/commands/base';
3+
import type { PromptData, PromptTemplateName } from '@ts/core/ai_integration/core/prompt_manager';
4+
5+
export class ChangeStyleCommand extends BaseCommand<
6+
ChangeStyleCommandParams,
7+
ChangeStyleCommandResult
8+
> {
9+
protected getTemplateName(): PromptTemplateName {
10+
return 'changeStyle';
11+
}
12+
13+
protected buildPromptData(params: ChangeStyleCommandParams): PromptData {
14+
return {
15+
system: { writingStyle: params.writingStyle },
16+
user: { text: params.text },
17+
};
18+
}
19+
20+
protected parseResult(response: string): ChangeStyleCommandResult {
21+
return response;
22+
}
23+
}

0 commit comments

Comments
 (0)