Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AI: Init, add .d.ts, add separate build, add tests #29345

Open
wants to merge 32 commits into
base: 25_1
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
942b90a
AI: Add ai-types
Mar 18, 2025
883d88d
feat(types): Add AIProvider
Mar 18, 2025
0ddfae4
feat(ai): Add PromptManager
Mar 18, 2025
5ce7e87
feat(ai): Add RequestManager
Mar 18, 2025
7b31a52
feat(ai): Add BaseCommand
Mar 18, 2025
2085739
feat(ai): Add AI
Mar 18, 2025
63e9c35
feat(ai): Add TranslateCommand
Mar 18, 2025
3693fd1
feat(ai): Update structure && Add AI
Mar 18, 2025
a666092
refactor(ai-types): Remove comments
Mar 19, 2025
f48076d
feat(ai && ai-types): Update imports
Mar 19, 2025
af6576e
feat(ai): Improve typing
Mar 19, 2025
87a4a3d
fix(ai): Fix imports
Mar 19, 2025
8fd3923
refactor(ai-types)
Mar 19, 2025
e6dfb79
feat(types): Add ai-types into dx.all.d.ts
Mar 19, 2025
e3d727c
feat(ai.js)
Mar 19, 2025
9848dba
fix(ai.d.ts): Move types to .d.ts && Fix imports && Regenerate all
Mar 19, 2025
e550970
feat && fix(ai &7 ai-types): Add implements && Fix AI type
Mar 19, 2025
3fd39a3
fiximport()
Mar 19, 2025
2b669a8
test(build)
Mar 19, 2025
199426a
feat(bundle): Add imports
Mar 19, 2025
1e626d2
refactor
Mar 19, 2025
12c52d0
feat(ai: tests): Add first tesrt
Mar 19, 2025
29dc40a
fix && improve(tests &7 types)
Mar 19, 2025
6d8c645
refactor(npm.js)
Mar 24, 2025
f24e15d
refactor
Mar 24, 2025
cb49448
feat(BaseCommand: tests)
Mar 24, 2025
73a3ee2
refactor(ai.d.ts): Move AI into packaje
Mar 25, 2025
d8b9aa9
feat(regenerate-all)
Mar 25, 2025
192eb27
feat(tests): Move AI qunit --> jest
Mar 25, 2025
0121bf4
refactor(tests): ai
Mar 25, 2025
e8ee19c
refactor(jest-tests): Move files
Mar 25, 2025
d69360f
feat(base command tests): Move to jest
Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/devextreme/build/gulp/js-bundles.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const namedDebug = lazyPipe()
.pipe(named, (file) => path.basename(file.path, path.extname(file.path)) + '.debug');

const BUNDLES = [
'/bundles/dx.ai.js',
'/bundles/dx.all.js',
'/bundles/dx.web.js',
'/bundles/dx.viz.js'
Expand Down Expand Up @@ -53,8 +54,10 @@ const jsBundlesProd = (src, dist, bundles) => (() =>
);

gulp.task('js-bundles-prod',
jsBundlesProd(ctx.TRANSPILED_PROD_RENOVATION_PATH,
ctx.RESULT_JS_PATH, BUNDLES
jsBundlesProd(
ctx.TRANSPILED_PROD_RENOVATION_PATH,
ctx.RESULT_JS_PATH,
BUNDLES,
)
);

Expand Down
182 changes: 182 additions & 0 deletions packages/devextreme/js/__internal/core/ai/commands/base.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import {
beforeEach,
describe,
expect,
it,
jest,
} from '@jest/globals';
import type { Prompt } from '@js/ai/ai';
import { BaseCommand } from '@ts/core/ai/commands/base';
import type { RequestCallbacks, RequestManager } from '@ts/core/ai/core//request_manager';
import type { PromptData, PromptManager, PromptTemplateName } from '@ts/core/ai/core/prompt_manager';

interface TestCommandParams {
first: string;
second: string;
}

class TestCommand extends BaseCommand {
getTemplateName(): PromptTemplateName {
return 'test' as PromptTemplateName;
}

buildPromptData(params: TestCommandParams): PromptData {
const data = {
user: { first: params?.first },
system: { second: params?.second },
};

return data;
}

parseResult(response: string): string {
return `Parsed result: ${response}`;
}
}

describe('BaseCommand Unit', () => {
// eslint-disable-next-line @typescript-eslint/init-declarations
let promptManager: PromptManager;
// eslint-disable-next-line @typescript-eslint/init-declarations
let requestManager: RequestManager;
// eslint-disable-next-line @typescript-eslint/init-declarations
let command: TestCommand;

beforeEach(() => {
promptManager = {
buildPrompt: (): Prompt => ({
system: 'systemMessage',
user: 'userMessage',
}),
} as unknown as PromptManager;

requestManager = {
sendRequest: (_: Prompt, callbacks: RequestCallbacks) => {
callbacks?.onComplete?.('AI response');

return (): void => {};
},
} as unknown as RequestManager;

command = new TestCommand(promptManager, requestManager);
});

describe('constructor', () => {
it('Stores PromptManager and RequestManager correctly', () => {
// @ts-ignore
expect(command.promptManager).toBe(promptManager);
// @ts-ignore
expect(command.requestManager).toBe(requestManager);
});
});

describe('execute', () => {
it('getTemplateName returns value correctly', () => {
const spy = jest.spyOn(command, 'getTemplateName');

command.execute({}, {});

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveReturnedWith('test');
});

it('buildPromptData receives and returns correct data', () => {
const params: TestCommandParams = { first: 'first', second: 'second' };
const spy = jest.spyOn(command, 'buildPromptData');

command.execute(params, {});

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(params);
expect(spy).toHaveReturnedWith({
user: { first: params.first },
system: { second: params.second },
});
});

it('parseResult receives correct value and returns expected result', () => {
const spy = jest.spyOn(command, 'parseResult');

command.execute({}, {});

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith('AI response');
expect(spy).toHaveReturnedWith('Parsed result: AI response');
});

it('callbacks are called correctly', () => {
const callbacks = {
onComplete: jest.fn(),
onError: jest.fn(),
onChunk: jest.fn(),
};

command.execute({}, callbacks as RequestCallbacks);

expect(callbacks.onComplete).toHaveBeenCalledTimes(1);
expect(callbacks.onError).toHaveBeenCalledTimes(0);
expect(callbacks.onChunk).toHaveBeenCalledTimes(0);
});

it('onComplete is called with parseResult output', () => {
const callbacks = {
onComplete: jest.fn(),
};

command.execute({}, callbacks as RequestCallbacks);

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

it('calls onError if request fails', () => {
const originalSendRequest = requestManager.sendRequest;

requestManager.sendRequest = (_, callbacks) => {
callbacks.onError?.(new Error('Test error'));

return (): void => {};
};

try {
const callbacks = {
onError: jest.fn(),
onComplete: jest.fn(),
};

command.execute({}, callbacks as RequestCallbacks);

expect(callbacks.onError).toHaveBeenCalledTimes(1);
expect(callbacks.onError).toHaveBeenCalledWith(new Error('Test error'));
expect(callbacks.onComplete).toHaveBeenCalledTimes(0);
} finally {
requestManager.sendRequest = originalSendRequest;
}
});

it('calls onChunk for each chunk', () => {
const originalSendRequest = requestManager.sendRequest;

requestManager.sendRequest = (_, callbacks) => {
callbacks.onChunk?.('first');
callbacks.onChunk?.('second');
callbacks.onComplete?.('first second');

return (): void => {};
};

try {
const onChunk = jest.fn();
const onComplete = jest.fn();

command.execute({}, { onChunk, onComplete });

expect(onChunk).toHaveBeenCalledTimes(2);
expect(onChunk).toHaveBeenNthCalledWith(1, 'first');
expect(onChunk).toHaveBeenNthCalledWith(2, 'second');
expect(onComplete).toHaveBeenCalledTimes(1);
} finally {
requestManager.sendRequest = originalSendRequest;
}
});
});
});
35 changes: 35 additions & 0 deletions packages/devextreme/js/__internal/core/ai/commands/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { TranslateResult } from '@ts/core/ai/commands/translate';
import type { PromptData, PromptManager, PromptTemplateName } from '@ts/core/ai/core/prompt_manager';
import type { RequestCallbacks, RequestManager } from '@ts/core/ai/core/request_manager';

export type BaseResult = TranslateResult;

export abstract class BaseCommand {
constructor(
protected promptManager: PromptManager,
protected requestManager: RequestManager,
) {}

public execute(params: unknown, callbacks: RequestCallbacks): () => void {
const templateName = this.getTemplateName();
const data = this.buildPromptData(params);

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

const abort = this.requestManager.sendRequest(prompt, {
onChunk: (chunk) => { callbacks?.onChunk?.(chunk); },
onComplete: (result) => {
const finalResponse = this.parseResult(result);

callbacks?.onComplete?.(finalResponse);
},
onError: (error) => { callbacks?.onError?.(error); },
});

return abort;
}

protected abstract getTemplateName(): PromptTemplateName;
protected abstract buildPromptData(params: unknown): PromptData;
protected abstract parseResult(response: string): BaseResult;
}
28 changes: 28 additions & 0 deletions packages/devextreme/js/__internal/core/ai/commands/translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BaseCommand } from '@ts/core/ai/commands/base';
import type { PromptData, PromptTemplateName } from '@ts/core/ai/core/prompt_manager';

export type TranslateResult = string;

export interface TranslateCommandParams {
text: string;
lang: string;
}

export class TranslateCommand extends BaseCommand {
protected getTemplateName(): PromptTemplateName {
return 'translate';
}

protected buildPromptData(params: TranslateCommandParams): PromptData {
return {
user: {
text: params.text,
lang: params.lang,
},
};
}

protected parseResult(response: string): TranslateResult {
return response;
}
}
37 changes: 37 additions & 0 deletions packages/devextreme/js/__internal/core/ai/core/ai.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
describe,
expect,
test,
} from '@jest/globals';
import type { ResponseParams } from '@js/ai/ai';
import { AI } from '@ts/core/ai/core/ai';

describe('AI Integration', () => {
test('sendRequest is called with correct parameters', (done) => {
expect.assertions(2);

const sendRequest = ({ prompt, onChunk }): ResponseParams => {
expect(prompt).toEqual({
system: 'You are a translation assistant.',
user: 'Translate text to fr language.',
});

expect(typeof onChunk).toBe('function');

return {
promise: Promise.resolve(),
abort: (): void => {},
};
};

const ai = new AI({ sendRequest });

ai.translate(
{ text: 'text', lang: 'fr' },
{
onComplete: () => done(),
onChunk: () => {},
},
);
});
});
35 changes: 35 additions & 0 deletions packages/devextreme/js/__internal/core/ai/core/ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { AIProvider } from '@js/ai/ai';
import type { BaseCommand } from '@ts/core/ai/commands/base';
import type { TranslateCommandParams } from '@ts/core/ai/commands/translate';
import { TranslateCommand } from '@ts/core/ai/commands/translate';
import { PromptManager } from '@ts/core/ai/core/prompt_manager';
import type { RequestCallbacks } from '@ts/core/ai/core/request_manager';
import { RequestManager } from '@ts/core/ai/core/request_manager';

export class AI {
private readonly promptManager: PromptManager;

private readonly requestManager: RequestManager;

constructor(provider: AIProvider) {
this.promptManager = new PromptManager();
this.requestManager = new RequestManager(provider);
}

private execute<F extends BaseCommand>(
Command: new (
promptManager: PromptManager,
requestManager: RequestManager,
) => F,
params: unknown,
callbacks: RequestCallbacks,
): () => void {
const command = new Command(this.promptManager, this.requestManager);

return command.execute(params, callbacks);
}

public translate(params: TranslateCommandParams, callbacks: RequestCallbacks): () => void {
return this.execute(TranslateCommand, params, callbacks);
}
}
52 changes: 52 additions & 0 deletions packages/devextreme/js/__internal/core/ai/core/prompt_manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { Prompt } from '@js/ai/ai';
import { templates } from '@ts/core/ai/templates/index';

export interface PromptData {
system?: Record<string, string>;
user?: Record<string, string>;
}

export interface PromptTemplate {
system: string;
user: string;
}

export type PromptTemplateName = 'translate';

export type PromptTemplates = Map<PromptTemplateName, PromptTemplate>;

export class PromptManager {
private readonly templates: PromptTemplates;

constructor() {
this.templates = new Map(Object.entries(templates) as [PromptTemplateName, PromptTemplate][]);
}

public buildPrompt(templateName: PromptTemplateName, data: PromptData): Prompt {
const template = this.templates.get(templateName);

if (!template) {
throw new Error('Template not found');
}

const system = this.replacePlaceholders(template.system, data.system);
const user = this.replacePlaceholders(template.user, data.user);

const prompt = { system, user };

return prompt;
}

private replacePlaceholders(prompt: string, placeholders?: Record<string, string>): string {
if (!placeholders) {
return prompt;
}

const result = Object.entries(placeholders).reduce(
(acc, [key, value]) => acc.split(`{{${key}}}`).join(value),
prompt,
);

return result;
}
}
Loading
Loading