@hazeljs/ai 0.2.0-beta.53 → 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 +1 -1
- package/dist/ai-enhanced.service.js +1 -1
- package/dist/ai-enhanced.test.d.ts +2 -0
- package/dist/ai-enhanced.test.d.ts.map +1 -0
- package/dist/ai-enhanced.test.js +587 -0
- package/dist/ai.decorator.test.d.ts +2 -0
- package/dist/ai.decorator.test.d.ts.map +1 -0
- package/dist/ai.decorator.test.js +189 -0
- package/dist/ai.module.test.d.ts +2 -0
- package/dist/ai.module.test.d.ts.map +1 -0
- package/dist/ai.module.test.js +23 -0
- package/dist/ai.service.js +1 -1
- package/dist/ai.service.test.d.ts +2 -0
- package/dist/ai.service.test.d.ts.map +1 -0
- package/dist/ai.service.test.js +222 -0
- package/dist/tracking/token.tracker.js +1 -1
- package/dist/vector/vector.service.js +1 -1
- package/package.json +2 -2
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.
|
|
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.
|
|
343
|
+
(0, core_1.Service)(),
|
|
344
344
|
__metadata("design:paramtypes", [token_tracker_1.TokenTracker, cache_1.CacheService])
|
|
345
345
|
], AIEnhancedService);
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|
+
});
|
package/dist/ai.service.js
CHANGED
|
@@ -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.
|
|
220
|
+
(0, core_1.Service)(),
|
|
221
221
|
__metadata("design:paramtypes", [Object])
|
|
222
222
|
], TokenTracker);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/ai",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
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": "
|
|
58
|
+
"gitHead": "f2e54f346eea552595a44607999454a9e388cb9e"
|
|
59
59
|
}
|