@hazeljs/ai 0.2.0-beta.54 → 0.2.0-beta.55

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/README.md CHANGED
@@ -494,4 +494,4 @@ Apache 2.0 © [HazelJS](https://hazeljs.com)
494
494
  - [Documentation](https://hazeljs.com/docs/packages/ai)
495
495
  - [GitHub](https://github.com/hazel-js/hazeljs)
496
496
  - [Issues](https://github.com/hazel-js/hazeljs/issues)
497
- - [Discord](https://discord.gg/hazeljs)
497
+ - [Discord](https://discord.com/channels/1448263814238965833/1448263814859456575)
@@ -340,6 +340,6 @@ let AIEnhancedService = class AIEnhancedService {
340
340
  };
341
341
  exports.AIEnhancedService = AIEnhancedService;
342
342
  exports.AIEnhancedService = AIEnhancedService = __decorate([
343
- (0, core_1.Injectable)(),
343
+ (0, core_1.Service)(),
344
344
  __metadata("design:paramtypes", [token_tracker_1.TokenTracker, cache_1.CacheService])
345
345
  ], AIEnhancedService);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ai-enhanced.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai-enhanced.test.d.ts","sourceRoot":"","sources":["../src/ai-enhanced.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,587 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const anthropic_provider_1 = require("./providers/anthropic.provider");
13
+ const gemini_provider_1 = require("./providers/gemini.provider");
14
+ const cohere_provider_1 = require("./providers/cohere.provider");
15
+ const vector_service_1 = require("./vector/vector.service");
16
+ const ai_function_decorator_1 = require("./decorators/ai-function.decorator");
17
+ const ai_validate_decorator_1 = require("./decorators/ai-validate.decorator");
18
+ // Mock the providers to avoid real API calls
19
+ jest.mock('./providers/anthropic.provider');
20
+ jest.mock('./providers/gemini.provider');
21
+ jest.mock('./providers/cohere.provider');
22
+ describe('AnthropicProvider', () => {
23
+ let provider;
24
+ let mockComplete;
25
+ let mockStreamComplete;
26
+ beforeEach(() => {
27
+ mockComplete = jest.fn().mockResolvedValue({
28
+ id: 'claude-123',
29
+ content: 'Mock response',
30
+ role: 'assistant',
31
+ model: 'claude-3-5-sonnet-20241022',
32
+ usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
33
+ finishReason: 'end_turn',
34
+ });
35
+ mockStreamComplete = jest.fn().mockImplementation(async function* () {
36
+ yield { id: 'claude-stream', content: 'Mock', delta: 'Mock', done: false };
37
+ yield {
38
+ id: 'claude-stream',
39
+ content: 'Mock stream',
40
+ delta: ' stream',
41
+ done: true,
42
+ usage: { promptTokens: 5, completionTokens: 2, totalTokens: 7 },
43
+ };
44
+ });
45
+ anthropic_provider_1.AnthropicProvider.mockImplementation(() => ({
46
+ complete: mockComplete,
47
+ streamComplete: mockStreamComplete,
48
+ embed: jest.fn().mockRejectedValue(new Error('Anthropic does not support embeddings')),
49
+ isAvailable: jest.fn().mockResolvedValue(true),
50
+ getSupportedModels: jest
51
+ .fn()
52
+ .mockReturnValue([
53
+ 'claude-3-5-sonnet-20241022',
54
+ 'claude-3-opus-20240229',
55
+ 'claude-3-sonnet-20240229',
56
+ 'claude-3-haiku-20240307',
57
+ ]),
58
+ name: 'anthropic',
59
+ }));
60
+ provider = new anthropic_provider_1.AnthropicProvider();
61
+ });
62
+ describe('complete', () => {
63
+ it('should generate completion', async () => {
64
+ const response = await provider.complete({
65
+ messages: [{ role: 'user', content: 'Hello' }],
66
+ });
67
+ expect(response).toBeDefined();
68
+ expect(response.id).toContain('claude-');
69
+ expect(response.content).toBeDefined();
70
+ expect(response.role).toBe('assistant');
71
+ expect(response.usage).toBeDefined();
72
+ });
73
+ it('should use specified model', async () => {
74
+ const response = await provider.complete({
75
+ messages: [{ role: 'user', content: 'Hello' }],
76
+ model: 'claude-3-sonnet-20240229',
77
+ });
78
+ expect(response.model).toBe('claude-3-5-sonnet-20241022');
79
+ });
80
+ });
81
+ describe('streamComplete', () => {
82
+ it('should stream completion', async () => {
83
+ const chunks = [];
84
+ for await (const chunk of provider.streamComplete({
85
+ messages: [{ role: 'user', content: 'Hello' }],
86
+ })) {
87
+ chunks.push(chunk.delta);
88
+ expect(chunk.id).toContain('claude-stream');
89
+ expect(chunk.content).toBeDefined();
90
+ }
91
+ expect(chunks.length).toBeGreaterThan(0);
92
+ });
93
+ it('should mark last chunk as done', async () => {
94
+ let lastChunk;
95
+ for await (const chunk of provider.streamComplete({
96
+ messages: [{ role: 'user', content: 'Hello' }],
97
+ })) {
98
+ lastChunk = chunk;
99
+ }
100
+ expect(lastChunk?.done).toBe(true);
101
+ expect(lastChunk?.usage).toBeDefined();
102
+ });
103
+ });
104
+ describe('embed', () => {
105
+ it('should throw error for embeddings', async () => {
106
+ await expect(provider.embed({ input: 'test' })).rejects.toThrow('Anthropic does not support embeddings');
107
+ });
108
+ });
109
+ describe('isAvailable', () => {
110
+ it('should check availability', async () => {
111
+ const available = await provider.isAvailable();
112
+ expect(typeof available).toBe('boolean');
113
+ });
114
+ });
115
+ describe('getSupportedModels', () => {
116
+ it('should return supported models', () => {
117
+ const models = provider.getSupportedModels();
118
+ expect(Array.isArray(models)).toBe(true);
119
+ expect(models).toContain('claude-3-opus-20240229');
120
+ expect(models).toContain('claude-3-sonnet-20240229');
121
+ });
122
+ });
123
+ });
124
+ describe('GeminiProvider', () => {
125
+ let provider;
126
+ let mockComplete;
127
+ let mockStreamComplete;
128
+ let mockEmbed;
129
+ beforeEach(() => {
130
+ mockComplete = jest.fn().mockResolvedValue({
131
+ id: 'gemini-123',
132
+ content: 'Mock response',
133
+ role: 'assistant',
134
+ model: 'gemini-pro',
135
+ usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
136
+ finishReason: 'STOP',
137
+ });
138
+ mockStreamComplete = jest.fn().mockImplementation(async function* () {
139
+ yield { id: 'gemini-stream', content: 'Mock', delta: 'Mock', done: false };
140
+ yield { id: 'gemini-stream', content: 'Mock stream', delta: ' stream', done: true };
141
+ });
142
+ mockEmbed = jest.fn().mockImplementation((request) => {
143
+ const inputArray = Array.isArray(request.input) ? request.input : [request.input];
144
+ return Promise.resolve({
145
+ embeddings: inputArray.map(() => new Array(768).fill(0.1)),
146
+ model: 'text-embedding-004',
147
+ usage: { promptTokens: 10, totalTokens: 10 },
148
+ });
149
+ });
150
+ gemini_provider_1.GeminiProvider.mockImplementation(() => ({
151
+ complete: mockComplete,
152
+ streamComplete: mockStreamComplete,
153
+ embed: mockEmbed,
154
+ isAvailable: jest.fn().mockResolvedValue(true),
155
+ getSupportedModels: jest
156
+ .fn()
157
+ .mockReturnValue([
158
+ 'gemini-pro',
159
+ 'gemini-pro-vision',
160
+ 'gemini-1.5-pro',
161
+ 'gemini-1.5-flash',
162
+ 'text-embedding-004',
163
+ ]),
164
+ name: 'gemini',
165
+ }));
166
+ provider = new gemini_provider_1.GeminiProvider();
167
+ });
168
+ describe('complete', () => {
169
+ it('should generate completion', async () => {
170
+ const response = await provider.complete({
171
+ messages: [{ role: 'user', content: 'Hello' }],
172
+ });
173
+ expect(response).toBeDefined();
174
+ expect(response.id).toContain('gemini-');
175
+ expect(response.content).toBeDefined();
176
+ expect(response.model).toBe('gemini-pro');
177
+ });
178
+ });
179
+ describe('streamComplete', () => {
180
+ it('should stream completion', async () => {
181
+ const chunks = [];
182
+ for await (const chunk of provider.streamComplete({
183
+ messages: [{ role: 'user', content: 'Hello' }],
184
+ })) {
185
+ chunks.push(chunk.delta);
186
+ }
187
+ expect(chunks.length).toBeGreaterThan(0);
188
+ });
189
+ });
190
+ describe('embed', () => {
191
+ it('should generate embeddings', async () => {
192
+ const response = await provider.embed({
193
+ input: 'test text',
194
+ });
195
+ expect(response).toBeDefined();
196
+ expect(response.embeddings).toHaveLength(1);
197
+ expect(response.embeddings[0]).toHaveLength(768);
198
+ expect(response.model).toBe('text-embedding-004');
199
+ });
200
+ it('should handle multiple inputs', async () => {
201
+ const response = await provider.embed({
202
+ input: ['text1', 'text2', 'text3'],
203
+ });
204
+ expect(response.embeddings).toHaveLength(3);
205
+ expect(response.usage).toBeDefined();
206
+ });
207
+ });
208
+ describe('getSupportedModels', () => {
209
+ it('should return supported models', () => {
210
+ const models = provider.getSupportedModels();
211
+ expect(models).toContain('gemini-pro');
212
+ expect(models).toContain('text-embedding-004');
213
+ });
214
+ });
215
+ });
216
+ describe('CohereProvider', () => {
217
+ let provider;
218
+ let mockComplete;
219
+ let mockStreamComplete;
220
+ let mockEmbed;
221
+ let mockRerank;
222
+ beforeEach(() => {
223
+ mockComplete = jest.fn().mockResolvedValue({
224
+ id: 'cohere-123',
225
+ content: 'Mock response',
226
+ role: 'assistant',
227
+ model: 'command',
228
+ usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 },
229
+ finishReason: 'COMPLETE',
230
+ });
231
+ mockStreamComplete = jest.fn().mockImplementation(async function* () {
232
+ yield { id: 'cohere-stream', content: 'Mock', delta: 'Mock', done: false };
233
+ yield { id: 'cohere-stream', content: 'Mock stream', delta: ' stream', done: true };
234
+ });
235
+ mockEmbed = jest.fn().mockImplementation((request) => {
236
+ const inputArray = Array.isArray(request.input) ? request.input : [request.input];
237
+ return Promise.resolve({
238
+ embeddings: inputArray.map(() => new Array(1024).fill(0.1)),
239
+ model: 'embed-english-v3.0',
240
+ usage: { promptTokens: 10, totalTokens: 10 },
241
+ });
242
+ });
243
+ mockRerank = jest.fn().mockImplementation((query, documents, topN) => {
244
+ const results = documents.map((doc, index) => ({
245
+ index,
246
+ score: 0.9 - index * 0.1,
247
+ document: doc,
248
+ }));
249
+ return Promise.resolve(topN ? results.slice(0, topN) : results);
250
+ });
251
+ cohere_provider_1.CohereProvider.mockImplementation(() => ({
252
+ complete: mockComplete,
253
+ streamComplete: mockStreamComplete,
254
+ embed: mockEmbed,
255
+ rerank: mockRerank,
256
+ isAvailable: jest.fn().mockResolvedValue(true),
257
+ getSupportedModels: jest
258
+ .fn()
259
+ .mockReturnValue([
260
+ 'command',
261
+ 'command-light',
262
+ 'command-r',
263
+ 'command-r-plus',
264
+ 'embed-english-v3.0',
265
+ 'embed-multilingual-v3.0',
266
+ ]),
267
+ name: 'cohere',
268
+ }));
269
+ provider = new cohere_provider_1.CohereProvider();
270
+ });
271
+ describe('complete', () => {
272
+ it('should generate completion', async () => {
273
+ const response = await provider.complete({
274
+ messages: [{ role: 'user', content: 'Hello' }],
275
+ });
276
+ expect(response).toBeDefined();
277
+ expect(response.id).toContain('cohere-');
278
+ expect(response.content).toBeDefined();
279
+ expect(response.model).toBe('command');
280
+ });
281
+ });
282
+ describe('streamComplete', () => {
283
+ it('should stream completion', async () => {
284
+ const chunks = [];
285
+ for await (const chunk of provider.streamComplete({
286
+ messages: [{ role: 'user', content: 'Hello' }],
287
+ })) {
288
+ chunks.push(chunk.delta);
289
+ }
290
+ expect(chunks.length).toBeGreaterThan(0);
291
+ });
292
+ });
293
+ describe('embed', () => {
294
+ it('should generate embeddings', async () => {
295
+ const response = await provider.embed({
296
+ input: 'test text',
297
+ });
298
+ expect(response).toBeDefined();
299
+ expect(response.embeddings).toHaveLength(1);
300
+ expect(response.embeddings[0]).toHaveLength(1024);
301
+ expect(response.model).toBe('embed-english-v3.0');
302
+ });
303
+ it('should handle multiple inputs', async () => {
304
+ const response = await provider.embed({
305
+ input: ['text1', 'text2'],
306
+ });
307
+ expect(response.embeddings).toHaveLength(2);
308
+ });
309
+ });
310
+ describe('rerank', () => {
311
+ it('should rerank documents', async () => {
312
+ const documents = ['doc1', 'doc2', 'doc3'];
313
+ const results = await provider.rerank('query', documents);
314
+ expect(results).toHaveLength(3);
315
+ expect(results[0]).toHaveProperty('index');
316
+ expect(results[0]).toHaveProperty('score');
317
+ expect(results[0]).toHaveProperty('document');
318
+ });
319
+ it('should limit results with topN', async () => {
320
+ const documents = ['doc1', 'doc2', 'doc3', 'doc4', 'doc5'];
321
+ const results = await provider.rerank('query', documents, 2);
322
+ expect(results).toHaveLength(2);
323
+ });
324
+ it('should sort by score descending', async () => {
325
+ const documents = ['doc1', 'doc2', 'doc3'];
326
+ const results = await provider.rerank('query', documents);
327
+ for (let i = 0; i < results.length - 1; i++) {
328
+ expect(results[i].score).toBeGreaterThanOrEqual(results[i + 1].score);
329
+ }
330
+ });
331
+ });
332
+ describe('getSupportedModels', () => {
333
+ it('should return supported models', () => {
334
+ const models = provider.getSupportedModels();
335
+ expect(models).toContain('command');
336
+ expect(models).toContain('embed-english-v3.0');
337
+ });
338
+ });
339
+ });
340
+ describe('VectorService', () => {
341
+ let service;
342
+ beforeEach(async () => {
343
+ service = new vector_service_1.VectorService();
344
+ await service.initialize({
345
+ database: 'pinecone',
346
+ index: 'test-index',
347
+ });
348
+ });
349
+ afterEach(async () => {
350
+ await service.clear();
351
+ });
352
+ describe('upsert', () => {
353
+ it('should upsert documents', async () => {
354
+ const documents = [
355
+ { id: '1', content: 'Document 1' },
356
+ { id: '2', content: 'Document 2' },
357
+ ];
358
+ await service.upsert(documents);
359
+ const doc1 = await service.get('1');
360
+ expect(doc1).toBeDefined();
361
+ expect(doc1?.content).toBe('Document 1');
362
+ });
363
+ it('should generate embeddings if not provided', async () => {
364
+ const documents = [{ id: '1', content: 'Test' }];
365
+ await service.upsert(documents);
366
+ const doc = await service.get('1');
367
+ expect(doc?.embedding).toBeDefined();
368
+ expect(doc?.embedding?.length).toBeGreaterThan(0);
369
+ });
370
+ it('should use provided embeddings', async () => {
371
+ const embedding = Array.from({ length: 1536 }, () => 0.5);
372
+ const documents = [{ id: '1', content: 'Test', embedding }];
373
+ await service.upsert(documents);
374
+ const doc = await service.get('1');
375
+ expect(doc?.embedding).toEqual(embedding);
376
+ });
377
+ });
378
+ describe('search', () => {
379
+ beforeEach(async () => {
380
+ await service.upsert([
381
+ { id: '1', content: 'Machine learning basics' },
382
+ { id: '2', content: 'Deep learning tutorial' },
383
+ { id: '3', content: 'Natural language processing' },
384
+ ]);
385
+ });
386
+ it('should search for similar documents', async () => {
387
+ const results = await service.search({
388
+ query: 'AI and machine learning',
389
+ topK: 2,
390
+ });
391
+ expect(results).toHaveLength(2);
392
+ expect(results[0]).toHaveProperty('id');
393
+ expect(results[0]).toHaveProperty('content');
394
+ expect(results[0]).toHaveProperty('score');
395
+ });
396
+ it('should return results sorted by score', async () => {
397
+ const results = await service.search({
398
+ query: 'test query',
399
+ topK: 3,
400
+ });
401
+ for (let i = 0; i < results.length - 1; i++) {
402
+ expect(results[i].score).toBeGreaterThanOrEqual(results[i + 1].score);
403
+ }
404
+ });
405
+ it('should respect topK parameter', async () => {
406
+ const results = await service.search({
407
+ query: 'test',
408
+ topK: 1,
409
+ });
410
+ expect(results).toHaveLength(1);
411
+ });
412
+ });
413
+ describe('delete', () => {
414
+ it('should delete documents', async () => {
415
+ await service.upsert([
416
+ { id: '1', content: 'Doc 1' },
417
+ { id: '2', content: 'Doc 2' },
418
+ ]);
419
+ await service.delete(['1']);
420
+ const doc = await service.get('1');
421
+ expect(doc).toBeNull();
422
+ const doc2 = await service.get('2');
423
+ expect(doc2).toBeDefined();
424
+ });
425
+ it('should delete multiple documents', async () => {
426
+ await service.upsert([
427
+ { id: '1', content: 'Doc 1' },
428
+ { id: '2', content: 'Doc 2' },
429
+ { id: '3', content: 'Doc 3' },
430
+ ]);
431
+ await service.delete(['1', '2']);
432
+ expect(await service.get('1')).toBeNull();
433
+ expect(await service.get('2')).toBeNull();
434
+ expect(await service.get('3')).toBeDefined();
435
+ });
436
+ });
437
+ describe('get', () => {
438
+ it('should get document by ID', async () => {
439
+ await service.upsert([{ id: '1', content: 'Test' }]);
440
+ const doc = await service.get('1');
441
+ expect(doc).toBeDefined();
442
+ expect(doc?.id).toBe('1');
443
+ expect(doc?.content).toBe('Test');
444
+ });
445
+ it('should return null for non-existent ID', async () => {
446
+ const doc = await service.get('nonexistent');
447
+ expect(doc).toBeNull();
448
+ });
449
+ });
450
+ describe('clear', () => {
451
+ it('should clear all documents', async () => {
452
+ await service.upsert([
453
+ { id: '1', content: 'Doc 1' },
454
+ { id: '2', content: 'Doc 2' },
455
+ ]);
456
+ await service.clear();
457
+ const stats = await service.getStats();
458
+ expect(stats.count).toBe(0);
459
+ });
460
+ });
461
+ describe('getStats', () => {
462
+ it('should return statistics', async () => {
463
+ await service.upsert([
464
+ { id: '1', content: 'Doc 1' },
465
+ { id: '2', content: 'Doc 2' },
466
+ ]);
467
+ const stats = await service.getStats();
468
+ expect(stats.count).toBe(2);
469
+ expect(stats.database).toBe('pinecone');
470
+ });
471
+ });
472
+ });
473
+ describe('AI Decorators', () => {
474
+ describe('@AIFunction', () => {
475
+ it('should store AI function metadata', () => {
476
+ class TestClass {
477
+ testMethod() {
478
+ return 'test';
479
+ }
480
+ }
481
+ __decorate([
482
+ (0, ai_function_decorator_1.AIFunction)({
483
+ provider: 'openai',
484
+ model: 'gpt-4',
485
+ streaming: true,
486
+ }),
487
+ __metadata("design:type", Function),
488
+ __metadata("design:paramtypes", []),
489
+ __metadata("design:returntype", void 0)
490
+ ], TestClass.prototype, "testMethod", null);
491
+ const instance = new TestClass();
492
+ const metadata = (0, ai_function_decorator_1.getAIFunctionMetadata)(instance, 'testMethod');
493
+ expect(metadata).toBeDefined();
494
+ expect(metadata?.provider).toBe('openai');
495
+ expect(metadata?.model).toBe('gpt-4');
496
+ expect(metadata?.streaming).toBe(true);
497
+ });
498
+ it('should apply default values', () => {
499
+ class TestClass {
500
+ testMethod() {
501
+ return 'test';
502
+ }
503
+ }
504
+ __decorate([
505
+ (0, ai_function_decorator_1.AIFunction)({
506
+ provider: 'anthropic',
507
+ model: 'claude-3-opus-20240229',
508
+ }),
509
+ __metadata("design:type", Function),
510
+ __metadata("design:paramtypes", []),
511
+ __metadata("design:returntype", void 0)
512
+ ], TestClass.prototype, "testMethod", null);
513
+ const instance = new TestClass();
514
+ const metadata = (0, ai_function_decorator_1.getAIFunctionMetadata)(instance, 'testMethod');
515
+ expect(metadata?.streaming).toBe(false);
516
+ expect(metadata?.temperature).toBe(0.7);
517
+ expect(metadata?.maxTokens).toBe(1000);
518
+ });
519
+ it('should check if method has AI function metadata', () => {
520
+ class TestClass {
521
+ decorated() { }
522
+ notDecorated() { }
523
+ }
524
+ __decorate([
525
+ (0, ai_function_decorator_1.AIFunction)({ provider: 'openai', model: 'gpt-4' }),
526
+ __metadata("design:type", Function),
527
+ __metadata("design:paramtypes", []),
528
+ __metadata("design:returntype", void 0)
529
+ ], TestClass.prototype, "decorated", null);
530
+ const instance = new TestClass();
531
+ expect((0, ai_function_decorator_1.hasAIFunctionMetadata)(instance, 'decorated')).toBe(true);
532
+ expect((0, ai_function_decorator_1.hasAIFunctionMetadata)(instance, 'notDecorated')).toBe(false);
533
+ });
534
+ });
535
+ describe('@AIPrompt', () => {
536
+ it('should mark parameter as prompt', () => {
537
+ // Decorator is applied, test passes if no errors
538
+ expect(true).toBe(true);
539
+ });
540
+ });
541
+ describe('@AIValidate', () => {
542
+ it('should store AI validation metadata', () => {
543
+ let TestDto = class TestDto {
544
+ };
545
+ TestDto = __decorate([
546
+ (0, ai_validate_decorator_1.AIValidate)({
547
+ provider: 'openai',
548
+ instruction: 'Validate email',
549
+ })
550
+ ], TestDto);
551
+ const metadata = (0, ai_validate_decorator_1.getAIValidationMetadata)(TestDto);
552
+ expect(metadata).toBeDefined();
553
+ expect(metadata?.provider).toBe('openai');
554
+ expect(metadata?.instruction).toBe('Validate email');
555
+ });
556
+ it('should apply default values', () => {
557
+ let TestDto = class TestDto {
558
+ };
559
+ TestDto = __decorate([
560
+ (0, ai_validate_decorator_1.AIValidate)({
561
+ provider: 'anthropic',
562
+ instruction: 'Validate',
563
+ })
564
+ ], TestDto);
565
+ const metadata = (0, ai_validate_decorator_1.getAIValidationMetadata)(TestDto);
566
+ expect(metadata?.model).toBe('gpt-3.5-turbo');
567
+ expect(metadata?.failOnInvalid).toBe(true);
568
+ });
569
+ it('should check if class has AI validation metadata', () => {
570
+ let DecoratedDto = class DecoratedDto {
571
+ };
572
+ DecoratedDto = __decorate([
573
+ (0, ai_validate_decorator_1.AIValidate)({ provider: 'openai', instruction: 'Test' })
574
+ ], DecoratedDto);
575
+ class NotDecoratedDto {
576
+ }
577
+ expect((0, ai_validate_decorator_1.hasAIValidationMetadata)(DecoratedDto)).toBe(true);
578
+ expect((0, ai_validate_decorator_1.hasAIValidationMetadata)(NotDecoratedDto)).toBe(false);
579
+ });
580
+ });
581
+ describe('@AIValidateProperty', () => {
582
+ it('should mark property for validation', () => {
583
+ // Decorator is applied, test passes if no errors
584
+ expect(true).toBe(true);
585
+ });
586
+ });
587
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ai.decorator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai.decorator.test.d.ts","sourceRoot":"","sources":["../src/ai.decorator.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,189 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const ai_decorator_1 = require("./ai.decorator");
13
+ const ai_service_1 = require("./ai.service");
14
+ const core_1 = require("@hazeljs/core");
15
+ const core_2 = require("@hazeljs/core");
16
+ // Mock the AIService
17
+ jest.mock('./ai.service', () => {
18
+ const mockAIService = jest.fn().mockImplementation(() => ({
19
+ executeTask: jest.fn().mockImplementation(async (config, input) => ({
20
+ data: `Processed: ${input}`,
21
+ })),
22
+ }));
23
+ return {
24
+ AIService: mockAIService,
25
+ };
26
+ });
27
+ describe('AITask Decorator', () => {
28
+ let container;
29
+ let mockAIService;
30
+ beforeEach(() => {
31
+ container = core_1.Container.getInstance();
32
+ container.clear();
33
+ // Create a mock instance
34
+ mockAIService = new ai_service_1.AIService();
35
+ // Register AIService with the mock instance
36
+ container.register(ai_service_1.AIService, mockAIService);
37
+ });
38
+ afterEach(() => {
39
+ container.clear();
40
+ jest.clearAllMocks();
41
+ });
42
+ it('should inject AIService', () => {
43
+ let TestClass = class TestClass {
44
+ constructor(aiService) {
45
+ this.aiService = aiService;
46
+ }
47
+ async testMethod(input) {
48
+ return input;
49
+ }
50
+ };
51
+ __decorate([
52
+ (0, ai_decorator_1.AITask)({
53
+ name: 'test-task',
54
+ prompt: 'Test prompt',
55
+ provider: 'openai',
56
+ model: 'gpt-3.5-turbo',
57
+ outputType: 'string',
58
+ }),
59
+ __metadata("design:type", Function),
60
+ __metadata("design:paramtypes", [String]),
61
+ __metadata("design:returntype", Promise)
62
+ ], TestClass.prototype, "testMethod", null);
63
+ TestClass = __decorate([
64
+ (0, core_2.Injectable)(),
65
+ __metadata("design:paramtypes", [ai_service_1.AIService])
66
+ ], TestClass);
67
+ // Register the test class
68
+ container.register(TestClass, new TestClass(mockAIService));
69
+ const instance = container.resolve(TestClass);
70
+ expect(instance.aiService).toBeDefined();
71
+ expect(instance.aiService).toBe(mockAIService);
72
+ });
73
+ it('should work with multiple dependencies', () => {
74
+ let OtherService = class OtherService {
75
+ };
76
+ OtherService = __decorate([
77
+ (0, core_2.Injectable)()
78
+ ], OtherService);
79
+ let TestClass = class TestClass {
80
+ constructor(aiService, otherService) {
81
+ this.aiService = aiService;
82
+ this.otherService = otherService;
83
+ }
84
+ async testMethod(input) {
85
+ return input;
86
+ }
87
+ };
88
+ __decorate([
89
+ (0, ai_decorator_1.AITask)({
90
+ name: 'test-task',
91
+ prompt: 'Test prompt',
92
+ provider: 'openai',
93
+ model: 'gpt-3.5-turbo',
94
+ outputType: 'string',
95
+ }),
96
+ __metadata("design:type", Function),
97
+ __metadata("design:paramtypes", [String]),
98
+ __metadata("design:returntype", Promise)
99
+ ], TestClass.prototype, "testMethod", null);
100
+ TestClass = __decorate([
101
+ (0, core_2.Injectable)(),
102
+ __metadata("design:paramtypes", [ai_service_1.AIService,
103
+ OtherService])
104
+ ], TestClass);
105
+ // Register both services
106
+ const otherService = new OtherService();
107
+ container.register(OtherService, otherService);
108
+ container.register(TestClass, new TestClass(mockAIService, otherService));
109
+ const instance = container.resolve(TestClass);
110
+ expect(instance.aiService).toBeDefined();
111
+ expect(instance.aiService).toBe(mockAIService);
112
+ expect(instance.otherService).toBeDefined();
113
+ expect(instance.otherService).toBe(otherService);
114
+ });
115
+ it('should throw error if AIService is not registered', async () => {
116
+ let TestClass = class TestClass {
117
+ constructor(aiService) {
118
+ this.aiService = aiService;
119
+ }
120
+ async testMethod(input) {
121
+ return input;
122
+ }
123
+ };
124
+ __decorate([
125
+ (0, ai_decorator_1.AITask)({
126
+ name: 'test-task',
127
+ prompt: 'Test prompt',
128
+ provider: 'openai',
129
+ model: 'gpt-3.5-turbo',
130
+ outputType: 'string',
131
+ }),
132
+ __metadata("design:type", Function),
133
+ __metadata("design:paramtypes", [String]),
134
+ __metadata("design:returntype", Promise)
135
+ ], TestClass.prototype, "testMethod", null);
136
+ TestClass = __decorate([
137
+ (0, core_2.Injectable)(),
138
+ __metadata("design:paramtypes", [ai_service_1.AIService])
139
+ ], TestClass);
140
+ container.clear();
141
+ // Don't register AIService
142
+ const instance = new TestClass(null);
143
+ container.register(TestClass, instance);
144
+ const resolved = container.resolve(TestClass);
145
+ // The error should be thrown when trying to execute the decorated method
146
+ await expect(resolved.testMethod('test')).rejects.toThrow('AI task execution failed: AI service not found. Make sure to inject AIService in the constructor.');
147
+ });
148
+ it('should execute AI task', async () => {
149
+ let TestClass = class TestClass {
150
+ constructor(aiService) {
151
+ this.aiService = aiService;
152
+ }
153
+ async testMethod(input) {
154
+ return input;
155
+ }
156
+ };
157
+ __decorate([
158
+ (0, ai_decorator_1.AITask)({
159
+ name: 'test-task',
160
+ prompt: 'Test prompt',
161
+ provider: 'openai',
162
+ model: 'gpt-4',
163
+ temperature: 0.7,
164
+ outputType: 'string',
165
+ }),
166
+ __metadata("design:type", Function),
167
+ __metadata("design:paramtypes", [String]),
168
+ __metadata("design:returntype", Promise)
169
+ ], TestClass.prototype, "testMethod", null);
170
+ TestClass = __decorate([
171
+ (0, core_2.Injectable)(),
172
+ __metadata("design:paramtypes", [ai_service_1.AIService])
173
+ ], TestClass);
174
+ // Register the test class with AIService
175
+ container.register(TestClass, new TestClass(mockAIService));
176
+ const instance = container.resolve(TestClass);
177
+ const result = await instance.testMethod('test input');
178
+ expect(result).toBeDefined();
179
+ expect(result).toBe('Processed: test input');
180
+ expect(mockAIService.executeTask).toHaveBeenCalledWith(expect.objectContaining({
181
+ name: 'test-task',
182
+ provider: 'openai',
183
+ model: 'gpt-4',
184
+ temperature: 0.7,
185
+ prompt: 'Test prompt',
186
+ outputType: 'string',
187
+ }), 'test input');
188
+ });
189
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ai.module.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai.module.test.d.ts","sourceRoot":"","sources":["../src/ai.module.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ai_module_1 = require("./ai.module");
4
+ const ai_service_1 = require("./ai.service");
5
+ const core_1 = require("@hazeljs/core");
6
+ describe('AIModule', () => {
7
+ it('should be defined', () => {
8
+ expect(ai_module_1.AIModule).toBeDefined();
9
+ });
10
+ it('should provide AIService', () => {
11
+ const app = new core_1.HazelApp(ai_module_1.AIModule);
12
+ const container = app.getContainer();
13
+ const aiService = container.resolve(ai_service_1.AIService);
14
+ expect(aiService).toBeInstanceOf(ai_service_1.AIService);
15
+ });
16
+ it('should provide AIService as singleton', () => {
17
+ const app = new core_1.HazelApp(ai_module_1.AIModule);
18
+ const container = app.getContainer();
19
+ const service1 = container.resolve(ai_service_1.AIService);
20
+ const service2 = container.resolve(ai_service_1.AIService);
21
+ expect(service1).toBe(service2);
22
+ });
23
+ });
@@ -256,6 +256,6 @@ let AIService = class AIService {
256
256
  };
257
257
  exports.AIService = AIService;
258
258
  exports.AIService = AIService = __decorate([
259
- (0, core_1.Injectable)(),
259
+ (0, core_1.Service)(),
260
260
  __metadata("design:paramtypes", [])
261
261
  ], AIService);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ai.service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ai.service.test.d.ts","sourceRoot":"","sources":["../src/ai.service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ai_service_1 = require("./ai.service");
4
+ // Mock OpenAI
5
+ const mockCreate = jest.fn();
6
+ jest.mock('openai', () => ({
7
+ __esModule: true,
8
+ default: jest.fn().mockImplementation(() => ({
9
+ chat: {
10
+ completions: {
11
+ create: mockCreate,
12
+ },
13
+ },
14
+ })),
15
+ }));
16
+ // Mock fetch
17
+ global.fetch = jest.fn();
18
+ describe('AIService', () => {
19
+ let service;
20
+ beforeEach(() => {
21
+ service = new ai_service_1.AIService();
22
+ jest.clearAllMocks();
23
+ mockCreate.mockReset();
24
+ });
25
+ describe('executeTask', () => {
26
+ const mockConfig = {
27
+ name: 'test-task',
28
+ provider: 'openai',
29
+ model: 'gpt-3.5-turbo',
30
+ prompt: 'Test prompt with {{input}}',
31
+ outputType: 'json',
32
+ temperature: 0.7,
33
+ maxTokens: 100,
34
+ };
35
+ const mockInput = { test: 'data' };
36
+ it('should execute task with OpenAI provider', async () => {
37
+ mockCreate.mockResolvedValueOnce({
38
+ choices: [
39
+ {
40
+ message: {
41
+ content: '{"result": "test"}',
42
+ },
43
+ },
44
+ ],
45
+ });
46
+ const result = await service.executeTask(mockConfig, mockInput);
47
+ expect(result).toEqual({ data: { result: 'test' } });
48
+ expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
49
+ model: 'gpt-3.5-turbo',
50
+ messages: [
51
+ expect.objectContaining({
52
+ role: 'system',
53
+ content: expect.stringContaining('Test prompt with'),
54
+ }),
55
+ ],
56
+ temperature: 0.7,
57
+ max_tokens: 100,
58
+ }));
59
+ });
60
+ it('should execute task with Ollama provider', async () => {
61
+ global.fetch.mockResolvedValueOnce({
62
+ json: () => Promise.resolve({ response: '{"result": "test"}' }),
63
+ });
64
+ const result = await service.executeTask({
65
+ ...mockConfig,
66
+ provider: 'ollama',
67
+ }, mockInput);
68
+ expect(result).toEqual({ data: { result: 'test' } });
69
+ expect(global.fetch).toHaveBeenCalledWith('http://localhost:11434/api/generate', expect.objectContaining({
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: expect.stringContaining('"model":"gpt-3.5-turbo"'),
73
+ }));
74
+ });
75
+ it('should execute task with custom provider', async () => {
76
+ global.fetch.mockResolvedValueOnce({
77
+ json: () => Promise.resolve({ result: '{"result":"test"}' }),
78
+ });
79
+ const result = await service.executeTask({
80
+ ...mockConfig,
81
+ provider: 'custom',
82
+ customProvider: {
83
+ url: 'http://custom-api.com',
84
+ headers: { 'X-API-Key': 'test' },
85
+ transformRequest: (input) => ({ transformed: input }),
86
+ transformResponse: (data) => data.result,
87
+ },
88
+ }, mockInput);
89
+ expect(result).toEqual({ data: { result: 'test' } });
90
+ expect(global.fetch).toHaveBeenCalledWith('http://custom-api.com', expect.objectContaining({
91
+ method: 'POST',
92
+ headers: expect.objectContaining({
93
+ 'Content-Type': 'application/json',
94
+ 'X-API-Key': 'test',
95
+ }),
96
+ body: JSON.stringify({ transformed: mockInput }),
97
+ }));
98
+ });
99
+ it('should handle unsupported provider', async () => {
100
+ const result = await service.executeTask({
101
+ ...mockConfig,
102
+ provider: 'anthropic',
103
+ }, mockInput);
104
+ expect(result).toEqual({
105
+ error: 'Provider anthropic not supported',
106
+ });
107
+ });
108
+ it('should handle OpenAI API errors', async () => {
109
+ mockCreate.mockRejectedValueOnce(new Error('API Error'));
110
+ const result = await service.executeTask(mockConfig, mockInput);
111
+ expect(result).toEqual({
112
+ error: 'API Error',
113
+ });
114
+ });
115
+ it('should handle Ollama API errors', async () => {
116
+ global.fetch.mockRejectedValueOnce(new Error('Network Error'));
117
+ const result = await service.executeTask({
118
+ ...mockConfig,
119
+ provider: 'ollama',
120
+ }, mockInput);
121
+ expect(result).toEqual({
122
+ error: 'Network Error',
123
+ });
124
+ });
125
+ it('should handle custom provider errors', async () => {
126
+ global.fetch.mockRejectedValueOnce(new Error('Custom API Error'));
127
+ const result = await service.executeTask({
128
+ ...mockConfig,
129
+ provider: 'custom',
130
+ customProvider: {
131
+ url: 'http://custom-api.com',
132
+ },
133
+ }, mockInput);
134
+ expect(result).toEqual({
135
+ error: 'Custom API Error',
136
+ });
137
+ });
138
+ it('should handle invalid JSON response', async () => {
139
+ mockCreate.mockResolvedValueOnce({
140
+ choices: [
141
+ {
142
+ message: {
143
+ content: 'invalid json',
144
+ },
145
+ },
146
+ ],
147
+ });
148
+ const result = await service.executeTask(mockConfig, mockInput);
149
+ expect(result.error).toContain('Failed to parse response');
150
+ });
151
+ it('should handle different output types', async () => {
152
+ // Test number output
153
+ mockCreate.mockResolvedValueOnce({
154
+ choices: [
155
+ {
156
+ message: {
157
+ content: '42',
158
+ },
159
+ },
160
+ ],
161
+ });
162
+ const numberResult = await service.executeTask({
163
+ ...mockConfig,
164
+ outputType: 'number',
165
+ }, mockInput);
166
+ expect(numberResult).toEqual({ data: 42 });
167
+ // Test boolean output
168
+ mockCreate.mockResolvedValueOnce({
169
+ choices: [
170
+ {
171
+ message: {
172
+ content: 'true',
173
+ },
174
+ },
175
+ ],
176
+ });
177
+ const booleanResult = await service.executeTask({
178
+ ...mockConfig,
179
+ outputType: 'boolean',
180
+ }, mockInput);
181
+ expect(booleanResult).toEqual({ data: true });
182
+ // Test string output
183
+ mockCreate.mockResolvedValueOnce({
184
+ choices: [
185
+ {
186
+ message: {
187
+ content: 'test string',
188
+ },
189
+ },
190
+ ],
191
+ });
192
+ const stringResult = await service.executeTask({
193
+ ...mockConfig,
194
+ outputType: 'string',
195
+ }, mockInput);
196
+ expect(stringResult).toEqual({ data: 'test string' });
197
+ });
198
+ it('should format prompt with context variables', async () => {
199
+ mockCreate.mockResolvedValueOnce({
200
+ choices: [
201
+ {
202
+ message: {
203
+ content: '{"result": "test"}',
204
+ },
205
+ },
206
+ ],
207
+ });
208
+ const config = {
209
+ ...mockConfig,
210
+ prompt: 'Task: {{taskName}}\nInput: {{input}}\nDescription: {{description}}',
211
+ };
212
+ await service.executeTask(config, mockInput);
213
+ expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({
214
+ messages: [
215
+ expect.objectContaining({
216
+ content: expect.stringContaining('Task: test-task'),
217
+ }),
218
+ ],
219
+ }));
220
+ });
221
+ });
222
+ });
@@ -217,6 +217,6 @@ let TokenTracker = class TokenTracker {
217
217
  };
218
218
  exports.TokenTracker = TokenTracker;
219
219
  exports.TokenTracker = TokenTracker = __decorate([
220
- (0, core_1.Injectable)(),
220
+ (0, core_1.Service)(),
221
221
  __metadata("design:paramtypes", [Object])
222
222
  ], TokenTracker);
@@ -159,5 +159,5 @@ let VectorService = class VectorService {
159
159
  };
160
160
  exports.VectorService = VectorService;
161
161
  exports.VectorService = VectorService = __decorate([
162
- (0, core_1.Injectable)()
162
+ (0, core_1.Service)()
163
163
  ], VectorService);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hazeljs/ai",
3
- "version": "0.2.0-beta.54",
3
+ "version": "0.2.0-beta.55",
4
4
  "description": "AI integration module for HazelJS framework",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -55,5 +55,5 @@
55
55
  "@hazeljs/cache": ">=0.2.0-beta.0",
56
56
  "@hazeljs/core": ">=0.2.0-beta.0"
57
57
  },
58
- "gitHead": "c593ce33447cdc62d7bd2386cc2db47840292fcb"
58
+ "gitHead": "f2e54f346eea552595a44607999454a9e388cb9e"
59
59
  }