@luanpoppe/ai 1.1.0 → 1.1.2
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/dist/@types/agent.d.ts +3 -0
- package/dist/@types/agent.d.ts.map +1 -0
- package/dist/@types/agent.js +3 -0
- package/dist/@types/agent.js.map +1 -0
- package/dist/@types/ai-call.d.ts +32 -0
- package/dist/@types/ai-call.d.ts.map +1 -0
- package/dist/@types/ai-call.js +3 -0
- package/dist/@types/ai-call.js.map +1 -0
- package/dist/@types/checkpointers.d.ts +106 -0
- package/dist/@types/checkpointers.d.ts.map +1 -0
- package/dist/@types/checkpointers.js +3 -0
- package/dist/@types/checkpointers.js.map +1 -0
- package/dist/ai.d.ts +49 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +206 -0
- package/dist/ai.js.map +1 -0
- package/dist/index.d.ts +10 -52
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -135
- package/dist/index.js.map +1 -1
- package/dist/langchain/checkpointers.d.ts +86 -0
- package/dist/langchain/checkpointers.d.ts.map +1 -0
- package/dist/langchain/checkpointers.js +242 -0
- package/dist/langchain/checkpointers.js.map +1 -0
- package/package.json +9 -1
- package/src/@types/agent.ts +3 -0
- package/src/@types/ai-call.ts +38 -0
- package/src/@types/checkpointers.ts +117 -0
- package/src/ai.ts +306 -0
- package/src/index.ts +34 -214
- package/src/langchain/checkpointers.ts +331 -0
- package/tests/e2e/ai-retry-fallback.test.ts +213 -0
- package/tests/e2e/ai.test.ts +125 -38
- package/tests/unit/index.test.ts +310 -13
- package/tests/unit/langchain/checkpointers.test.ts +147 -0
- package/tests/unit/langchain/tools.test.ts +2 -1
package/tests/unit/index.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { AI } from "../../src/index";
|
|
|
2
2
|
import { AIModels } from "../../src/langchain/models";
|
|
3
3
|
import { createAgent } from "langchain";
|
|
4
4
|
import { AIMessages } from "../../src/langchain/messages";
|
|
5
|
+
import { AIMemory } from "../../src/langchain/checkpointers";
|
|
5
6
|
import z from "zod";
|
|
6
7
|
|
|
7
8
|
// Mock das dependências
|
|
@@ -11,10 +12,6 @@ vi.mock("langchain", async () => {
|
|
|
11
12
|
...actual,
|
|
12
13
|
createAgent: vi.fn(),
|
|
13
14
|
modelRetryMiddleware: vi.fn((config) => ({ type: "retry", ...config })),
|
|
14
|
-
modelFallbackMiddleware: vi.fn((...models) => ({
|
|
15
|
-
type: "fallback",
|
|
16
|
-
models,
|
|
17
|
-
})),
|
|
18
15
|
};
|
|
19
16
|
});
|
|
20
17
|
|
|
@@ -22,9 +19,23 @@ vi.mock("../../src/langchain/models", () => ({
|
|
|
22
19
|
AIModels: {
|
|
23
20
|
gpt: vi.fn(),
|
|
24
21
|
gemini: vi.fn(),
|
|
22
|
+
openrouter: vi.fn(),
|
|
25
23
|
},
|
|
26
24
|
}));
|
|
27
25
|
|
|
26
|
+
vi.mock("../../src/langchain/checkpointers", async () => {
|
|
27
|
+
const actual = await vi.importActual("../../src/langchain/checkpointers");
|
|
28
|
+
return {
|
|
29
|
+
...actual,
|
|
30
|
+
AIMemory: vi.fn().mockImplementation(function () {
|
|
31
|
+
return {
|
|
32
|
+
getCheckpointer: vi.fn().mockResolvedValue({}),
|
|
33
|
+
setAgent: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
28
39
|
describe("AI", () => {
|
|
29
40
|
let ai: AI;
|
|
30
41
|
const mockTokens = {
|
|
@@ -52,6 +63,30 @@ describe("AI", () => {
|
|
|
52
63
|
const instance = new AI({ googleGeminiToken: "test-token" });
|
|
53
64
|
expect(instance).toBeInstanceOf(AI);
|
|
54
65
|
});
|
|
66
|
+
|
|
67
|
+
it("deve criar uma instância com memory config e expor getter memory", () => {
|
|
68
|
+
const instance = new AI({
|
|
69
|
+
openAIApiKey: "test-key",
|
|
70
|
+
memory: { type: "memory" },
|
|
71
|
+
});
|
|
72
|
+
expect(instance).toBeInstanceOf(AI);
|
|
73
|
+
expect(instance.memory).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("deve criar uma instância com checkpointer", () => {
|
|
77
|
+
const mockCheckpointer = {} as any;
|
|
78
|
+
const instance = new AI({
|
|
79
|
+
openAIApiKey: "test-key",
|
|
80
|
+
checkpointer: mockCheckpointer,
|
|
81
|
+
});
|
|
82
|
+
expect(instance).toBeInstanceOf(AI);
|
|
83
|
+
expect(() => instance.memory).toThrow("memory não está configurado");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("memory getter deve lançar erro quando memory não está configurado", () => {
|
|
87
|
+
const instance = new AI({ openAIApiKey: "test-key" });
|
|
88
|
+
expect(() => instance.memory).toThrow("memory não está configurado");
|
|
89
|
+
});
|
|
55
90
|
});
|
|
56
91
|
|
|
57
92
|
describe("call", () => {
|
|
@@ -159,14 +194,17 @@ describe("AI", () => {
|
|
|
159
194
|
invoke: vi.fn().mockResolvedValue(mockResponse),
|
|
160
195
|
} as any);
|
|
161
196
|
|
|
197
|
+
const { modelRetryMiddleware } = await import("langchain");
|
|
198
|
+
|
|
162
199
|
await ai.call({
|
|
163
200
|
aiModel: "gpt-4",
|
|
164
201
|
messages: mockMessages,
|
|
165
202
|
maxRetries: 5,
|
|
166
203
|
});
|
|
167
204
|
|
|
168
|
-
|
|
169
|
-
|
|
205
|
+
expect(modelRetryMiddleware).toHaveBeenCalledWith(
|
|
206
|
+
expect.objectContaining({ maxRetries: 5 }),
|
|
207
|
+
);
|
|
170
208
|
});
|
|
171
209
|
|
|
172
210
|
it("deve usar modelo GPT quando aiModel começa com 'gpt'", async () => {
|
|
@@ -222,7 +260,7 @@ describe("AI", () => {
|
|
|
222
260
|
ai.call({
|
|
223
261
|
aiModel: "unsupported-model" as any,
|
|
224
262
|
messages: mockMessages,
|
|
225
|
-
})
|
|
263
|
+
}),
|
|
226
264
|
).rejects.toThrow("Model not supported");
|
|
227
265
|
});
|
|
228
266
|
|
|
@@ -254,6 +292,231 @@ describe("AI", () => {
|
|
|
254
292
|
temperature: 0.7,
|
|
255
293
|
});
|
|
256
294
|
});
|
|
295
|
+
|
|
296
|
+
it("deve lançar erro quando memory está ativo e threadId não é fornecido", async () => {
|
|
297
|
+
const aiWithMemory = new AI({
|
|
298
|
+
openAIApiKey: "test-key",
|
|
299
|
+
memory: { type: "memory" },
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await expect(
|
|
303
|
+
aiWithMemory.call({
|
|
304
|
+
aiModel: "gpt-4",
|
|
305
|
+
messages: [AIMessages.human("Olá")],
|
|
306
|
+
}),
|
|
307
|
+
).rejects.toThrow("threadId é obrigatório");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("deve fazer fallback para o próximo modelo quando o principal falhar", async () => {
|
|
311
|
+
const mockModel = {} as any;
|
|
312
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
313
|
+
const mockResponse = {
|
|
314
|
+
messages: [{ content: "Resposta do fallback" } as any],
|
|
315
|
+
};
|
|
316
|
+
const error = new Error("Modelo principal falhou");
|
|
317
|
+
|
|
318
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
319
|
+
vi.mocked(AIModels.gemini).mockReturnValue(mockModel);
|
|
320
|
+
|
|
321
|
+
let createCount = 0;
|
|
322
|
+
vi.mocked(createAgent).mockImplementation(() => {
|
|
323
|
+
createCount++;
|
|
324
|
+
return {
|
|
325
|
+
invoke: vi.fn().mockImplementation(() => {
|
|
326
|
+
if (createCount === 1) return Promise.reject(error);
|
|
327
|
+
return Promise.resolve(mockResponse);
|
|
328
|
+
}),
|
|
329
|
+
} as any;
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const result = await ai.call({
|
|
333
|
+
aiModel: "gpt-4",
|
|
334
|
+
messages: mockMessages,
|
|
335
|
+
aiModelsFallback: ["gemini-2.5-flash"],
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(createAgent).toHaveBeenCalledTimes(2);
|
|
339
|
+
expect(AIModels.gpt).toHaveBeenCalled();
|
|
340
|
+
expect(AIModels.gemini).toHaveBeenCalled();
|
|
341
|
+
expect(result.text).toBe("Resposta do fallback");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("deve fazer fallback com modelo OpenRouter na lista", async () => {
|
|
345
|
+
const mockModel = {} as any;
|
|
346
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
347
|
+
const mockResponse = {
|
|
348
|
+
messages: [{ content: "Resposta OpenRouter" } as any],
|
|
349
|
+
};
|
|
350
|
+
const error = new Error("GPT falhou");
|
|
351
|
+
|
|
352
|
+
const aiWithOpenRouter = new AI({
|
|
353
|
+
openAIApiKey: "test-key",
|
|
354
|
+
openRouterApiKey: "test-openrouter-key",
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
358
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
359
|
+
|
|
360
|
+
let callCount = 0;
|
|
361
|
+
vi.mocked(createAgent).mockImplementation(() => {
|
|
362
|
+
callCount++;
|
|
363
|
+
return {
|
|
364
|
+
invoke: vi.fn().mockImplementation(() => {
|
|
365
|
+
if (callCount === 1) return Promise.reject(error);
|
|
366
|
+
return Promise.resolve(mockResponse);
|
|
367
|
+
}),
|
|
368
|
+
} as any;
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = await aiWithOpenRouter.call({
|
|
372
|
+
aiModel: "gpt-4",
|
|
373
|
+
messages: mockMessages,
|
|
374
|
+
aiModelsFallback: ["openrouter/openai/gpt-5-nano"],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(AIModels.openrouter).toHaveBeenCalledWith(
|
|
378
|
+
expect.objectContaining({
|
|
379
|
+
model: "openai/gpt-5-nano",
|
|
380
|
+
apiKey: "test-openrouter-key",
|
|
381
|
+
}),
|
|
382
|
+
);
|
|
383
|
+
expect(result.text).toBe("Resposta OpenRouter");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("deve usar aiModelsFallback do construtor quando não passado no método", async () => {
|
|
387
|
+
const mockModel = {} as any;
|
|
388
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
389
|
+
const mockResponse = {
|
|
390
|
+
messages: [{ content: "Resposta fallback default" } as any],
|
|
391
|
+
};
|
|
392
|
+
const error = new Error("Modelo principal falhou");
|
|
393
|
+
|
|
394
|
+
const aiWithDefaultFallback = new AI({
|
|
395
|
+
openAIApiKey: "test-key",
|
|
396
|
+
aiModelsFallback: ["gemini-2.5-flash"],
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
400
|
+
vi.mocked(AIModels.gemini).mockReturnValue(mockModel);
|
|
401
|
+
|
|
402
|
+
let createCount = 0;
|
|
403
|
+
vi.mocked(createAgent).mockImplementation(() => {
|
|
404
|
+
createCount++;
|
|
405
|
+
return {
|
|
406
|
+
invoke: vi.fn().mockImplementation(() => {
|
|
407
|
+
if (createCount === 1) return Promise.reject(error);
|
|
408
|
+
return Promise.resolve(mockResponse);
|
|
409
|
+
}),
|
|
410
|
+
} as any;
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const result = await aiWithDefaultFallback.call({
|
|
414
|
+
aiModel: "gpt-4",
|
|
415
|
+
messages: mockMessages,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
expect(createAgent).toHaveBeenCalledTimes(2);
|
|
419
|
+
expect(result.text).toBe("Resposta fallback default");
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("deve usar aiModelsFallback do método quando passado, sobrescrevendo o do construtor", async () => {
|
|
423
|
+
const mockModel = {} as any;
|
|
424
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
425
|
+
const mockResponse = {
|
|
426
|
+
messages: [{ content: "Resposta fallback do método" } as any],
|
|
427
|
+
};
|
|
428
|
+
const error = new Error("Modelo principal falhou");
|
|
429
|
+
|
|
430
|
+
const aiWithDefaultFallback = new AI({
|
|
431
|
+
openAIApiKey: "test-key",
|
|
432
|
+
openRouterApiKey: "test-openrouter-key",
|
|
433
|
+
aiModelsFallback: ["gemini-2.5-flash"],
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
437
|
+
vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
|
|
438
|
+
|
|
439
|
+
let createCount = 0;
|
|
440
|
+
vi.mocked(createAgent).mockImplementation(() => {
|
|
441
|
+
createCount++;
|
|
442
|
+
return {
|
|
443
|
+
invoke: vi.fn().mockImplementation(() => {
|
|
444
|
+
if (createCount === 1) return Promise.reject(error);
|
|
445
|
+
return Promise.resolve(mockResponse);
|
|
446
|
+
}),
|
|
447
|
+
} as any;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const result = await aiWithDefaultFallback.call({
|
|
451
|
+
aiModel: "gpt-4",
|
|
452
|
+
messages: mockMessages,
|
|
453
|
+
aiModelsFallback: ["openrouter/openai/gpt-5-nano"],
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(AIModels.openrouter).toHaveBeenCalled();
|
|
457
|
+
expect(result.text).toBe("Resposta fallback do método");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it("deve lançar exceção quando todos os modelos falharem", async () => {
|
|
461
|
+
const mockModel = {} as any;
|
|
462
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
463
|
+
const error = new Error("Todos os modelos falharam");
|
|
464
|
+
|
|
465
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
466
|
+
vi.mocked(AIModels.gemini).mockReturnValue(mockModel);
|
|
467
|
+
|
|
468
|
+
vi.mocked(createAgent).mockReturnValue({
|
|
469
|
+
invoke: vi.fn().mockRejectedValue(error),
|
|
470
|
+
} as any);
|
|
471
|
+
|
|
472
|
+
await expect(
|
|
473
|
+
ai.call({
|
|
474
|
+
aiModel: "gpt-4",
|
|
475
|
+
messages: mockMessages,
|
|
476
|
+
aiModelsFallback: ["gemini-2.5-flash"],
|
|
477
|
+
}),
|
|
478
|
+
).rejects.toThrow("Todos os modelos falharam");
|
|
479
|
+
|
|
480
|
+
expect(createAgent).toHaveBeenCalledTimes(2);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("deve passar checkpointer e thread_id quando memory e threadId são fornecidos", async () => {
|
|
484
|
+
const mockCheckpointer = {};
|
|
485
|
+
vi.mocked(AIMemory).mockImplementation(function () {
|
|
486
|
+
return {
|
|
487
|
+
getCheckpointer: vi.fn().mockResolvedValue(mockCheckpointer),
|
|
488
|
+
setAgent: vi.fn(),
|
|
489
|
+
} as any;
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const aiWithMemory = new AI({
|
|
493
|
+
openAIApiKey: "test-key",
|
|
494
|
+
memory: { type: "memory" },
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const mockModel = {} as any;
|
|
498
|
+
const mockMessages = [AIMessages.human("Olá")];
|
|
499
|
+
const mockResponse = {
|
|
500
|
+
messages: [mockMessages[0], { content: "Resposta" } as any],
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
504
|
+
const mockInvoke = vi.fn().mockResolvedValue(mockResponse);
|
|
505
|
+
vi.mocked(createAgent).mockReturnValue({ invoke: mockInvoke } as any);
|
|
506
|
+
|
|
507
|
+
await aiWithMemory.call({
|
|
508
|
+
aiModel: "gpt-4",
|
|
509
|
+
messages: mockMessages,
|
|
510
|
+
threadId: "thread-123",
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const callArgs = vi.mocked(createAgent).mock.calls[0][0];
|
|
514
|
+
expect(callArgs.checkpointer).toBe(mockCheckpointer);
|
|
515
|
+
expect(mockInvoke).toHaveBeenCalledWith(
|
|
516
|
+
{ messages: mockMessages },
|
|
517
|
+
{ configurable: { thread_id: "thread-123" } },
|
|
518
|
+
);
|
|
519
|
+
});
|
|
257
520
|
});
|
|
258
521
|
|
|
259
522
|
describe("callStructuredOutput", () => {
|
|
@@ -284,6 +547,40 @@ describe("AI", () => {
|
|
|
284
547
|
expect(result.response).toEqual(mockStructuredResponse);
|
|
285
548
|
});
|
|
286
549
|
|
|
550
|
+
it("deve fazer fallback no callStructuredOutput quando o modelo principal falhar", async () => {
|
|
551
|
+
const mockModel = {} as any;
|
|
552
|
+
const mockMessages = [AIMessages.human("Teste")];
|
|
553
|
+
const outputSchema = z.object({ name: z.string() });
|
|
554
|
+
const mockStructuredResponse = { name: "Fallback" };
|
|
555
|
+
const error = new Error("Modelo principal falhou");
|
|
556
|
+
|
|
557
|
+
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
558
|
+
vi.mocked(AIModels.gemini).mockReturnValue(mockModel);
|
|
559
|
+
|
|
560
|
+
let createCount = 0;
|
|
561
|
+
vi.mocked(createAgent).mockImplementation(() => {
|
|
562
|
+
createCount++;
|
|
563
|
+
return {
|
|
564
|
+
invoke: vi.fn().mockImplementation(() => {
|
|
565
|
+
if (createCount === 1) return Promise.reject(error);
|
|
566
|
+
return Promise.resolve({
|
|
567
|
+
structuredResponse: mockStructuredResponse,
|
|
568
|
+
});
|
|
569
|
+
}),
|
|
570
|
+
} as any;
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const result = await ai.callStructuredOutput({
|
|
574
|
+
aiModel: "gpt-4",
|
|
575
|
+
messages: mockMessages,
|
|
576
|
+
outputSchema,
|
|
577
|
+
aiModelsFallback: ["gemini-2.5-flash"],
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
expect(createAgent).toHaveBeenCalledTimes(2);
|
|
581
|
+
expect(result.response).toEqual(mockStructuredResponse);
|
|
582
|
+
});
|
|
583
|
+
|
|
287
584
|
it("deve validar o schema e lançar erro se inválido", async () => {
|
|
288
585
|
const mockModel = {} as any;
|
|
289
586
|
const mockMessages = [AIMessages.human("Teste")];
|
|
@@ -305,13 +602,13 @@ describe("AI", () => {
|
|
|
305
602
|
aiModel: "gpt-4",
|
|
306
603
|
messages: mockMessages,
|
|
307
604
|
outputSchema,
|
|
308
|
-
})
|
|
605
|
+
}),
|
|
309
606
|
).rejects.toThrow();
|
|
310
607
|
});
|
|
311
608
|
});
|
|
312
609
|
|
|
313
610
|
describe("getRawAgent", () => {
|
|
314
|
-
it("deve retornar um agente sem outputSchema", () => {
|
|
611
|
+
it("deve retornar um agente sem outputSchema", async () => {
|
|
315
612
|
const mockModel = {} as any;
|
|
316
613
|
const mockMessages = [AIMessages.human("Teste")];
|
|
317
614
|
const mockAgent = {
|
|
@@ -321,7 +618,7 @@ describe("AI", () => {
|
|
|
321
618
|
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
322
619
|
vi.mocked(createAgent).mockReturnValue(mockAgent as any);
|
|
323
620
|
|
|
324
|
-
const result = ai.getRawAgent({
|
|
621
|
+
const result = await ai.getRawAgent({
|
|
325
622
|
aiModel: "gpt-4",
|
|
326
623
|
messages: mockMessages,
|
|
327
624
|
});
|
|
@@ -330,7 +627,7 @@ describe("AI", () => {
|
|
|
330
627
|
expect(result.agent).toBe(mockAgent);
|
|
331
628
|
});
|
|
332
629
|
|
|
333
|
-
it("deve retornar um agente com outputSchema quando fornecido", () => {
|
|
630
|
+
it("deve retornar um agente com outputSchema quando fornecido", async () => {
|
|
334
631
|
const mockModel = {} as any;
|
|
335
632
|
const mockMessages = [AIMessages.human("Teste")];
|
|
336
633
|
const outputSchema = z.object({ result: z.string() });
|
|
@@ -341,12 +638,12 @@ describe("AI", () => {
|
|
|
341
638
|
vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
|
|
342
639
|
vi.mocked(createAgent).mockReturnValue(mockAgent as any);
|
|
343
640
|
|
|
344
|
-
const result = ai.getRawAgent(
|
|
641
|
+
const result = await ai.getRawAgent(
|
|
345
642
|
{
|
|
346
643
|
aiModel: "gpt-4",
|
|
347
644
|
messages: mockMessages,
|
|
348
645
|
},
|
|
349
|
-
outputSchema
|
|
646
|
+
outputSchema,
|
|
350
647
|
);
|
|
351
648
|
|
|
352
649
|
expect(createAgent).toHaveBeenCalled();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { AIMemory } from "../../../src/langchain/checkpointers";
|
|
2
|
+
import { MemorySaver } from "@langchain/langgraph";
|
|
3
|
+
import { describe, it, expect, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
describe("AIMemory", () => {
|
|
6
|
+
describe("getCheckpointer", () => {
|
|
7
|
+
it("deve criar MemorySaver para type memory", async () => {
|
|
8
|
+
const memory = new AIMemory({ type: "memory" });
|
|
9
|
+
const checkpointer = await memory.getCheckpointer();
|
|
10
|
+
expect(checkpointer).toBeInstanceOf(MemorySaver);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("deve retornar a mesma instância em chamadas subsequentes", async () => {
|
|
14
|
+
const memory = new AIMemory({ type: "memory" });
|
|
15
|
+
const cp1 = await memory.getCheckpointer();
|
|
16
|
+
const cp2 = await memory.getCheckpointer();
|
|
17
|
+
expect(cp1).toBe(cp2);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("deve criar MongoDBSaver com url quando pacotes estão instalados", async () => {
|
|
21
|
+
const memory = new AIMemory({
|
|
22
|
+
type: "mongodb",
|
|
23
|
+
url: "mongodb://localhost:27017/test",
|
|
24
|
+
});
|
|
25
|
+
const checkpointer = await memory.getCheckpointer();
|
|
26
|
+
expect(checkpointer).toBeDefined();
|
|
27
|
+
expect(checkpointer).toHaveProperty("get");
|
|
28
|
+
expect(checkpointer).toHaveProperty("put");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("getHistory", () => {
|
|
33
|
+
it("deve retornar fullHistory e messages via graph.getStateHistory", async () => {
|
|
34
|
+
const memory = new AIMemory({ type: "memory" });
|
|
35
|
+
const mockSnapshot1 = {
|
|
36
|
+
values: { messages: [{ role: "user", content: "oi" }] },
|
|
37
|
+
config: { configurable: { thread_id: "1" } },
|
|
38
|
+
next: [],
|
|
39
|
+
tasks: [],
|
|
40
|
+
createdAt: "2024-01-01T10:00:00Z",
|
|
41
|
+
};
|
|
42
|
+
const mockSnapshot2 = {
|
|
43
|
+
values: {
|
|
44
|
+
messages: [
|
|
45
|
+
{ role: "user", content: "oi" },
|
|
46
|
+
{ role: "assistant", content: "olá" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
config: { configurable: { thread_id: "1" } },
|
|
50
|
+
next: [],
|
|
51
|
+
tasks: [],
|
|
52
|
+
createdAt: "2024-01-01T10:01:00Z",
|
|
53
|
+
};
|
|
54
|
+
// getStateHistory retorna mais recente primeiro: snapshot2 (2 msgs) depois snapshot1 (1 msg)
|
|
55
|
+
const mockGraph = {
|
|
56
|
+
getStateHistory: vi.fn(async function* () {
|
|
57
|
+
yield mockSnapshot2;
|
|
58
|
+
yield mockSnapshot1;
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
const result = await memory.getHistory("1", mockGraph as any);
|
|
62
|
+
expect(result.fullHistory).toHaveLength(2);
|
|
63
|
+
expect(result.fullHistory[0]).toEqual(mockSnapshot2);
|
|
64
|
+
expect(result.fullHistory[1]).toEqual(mockSnapshot1);
|
|
65
|
+
expect(result.messages).toHaveLength(2);
|
|
66
|
+
expect(result.messages[0]).toEqual({
|
|
67
|
+
role: "human",
|
|
68
|
+
createdAt: "2024-01-01T10:00:00Z",
|
|
69
|
+
content: "oi",
|
|
70
|
+
});
|
|
71
|
+
expect(result.messages[1]).toEqual({
|
|
72
|
+
role: "ai",
|
|
73
|
+
createdAt: "2024-01-01T10:01:00Z",
|
|
74
|
+
content: "olá",
|
|
75
|
+
});
|
|
76
|
+
expect(mockGraph.getStateHistory).toHaveBeenCalledWith({
|
|
77
|
+
configurable: { thread_id: "1" },
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("deve usar agent do setAgent quando graph não é passado", async () => {
|
|
82
|
+
const memory = new AIMemory({ type: "memory" });
|
|
83
|
+
const mockGraph = {
|
|
84
|
+
getStateHistory: vi.fn(async function* () {
|
|
85
|
+
yield {
|
|
86
|
+
values: { messages: [{ role: "user", content: "teste" }] },
|
|
87
|
+
createdAt: "2024-01-01T10:00:00Z",
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
};
|
|
91
|
+
memory.setAgent(mockGraph as any);
|
|
92
|
+
const result = await memory.getHistory("1");
|
|
93
|
+
expect(result.messages).toHaveLength(1);
|
|
94
|
+
expect(result.messages[0].content).toBe("teste");
|
|
95
|
+
expect(mockGraph.getStateHistory).toHaveBeenCalledWith({
|
|
96
|
+
configurable: { thread_id: "1" },
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("deve lançar erro quando graph não é passado e agent não foi definido", async () => {
|
|
101
|
+
const memory = new AIMemory({ type: "memory" });
|
|
102
|
+
await expect(memory.getHistory("1")).rejects.toThrow(
|
|
103
|
+
"É necessário passar graph em getHistory ou definir o agent"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("getState", () => {
|
|
109
|
+
it("deve retornar estado atual via graph.getState quando disponível", async () => {
|
|
110
|
+
const memory = new AIMemory({ type: "memory" });
|
|
111
|
+
const mockState = {
|
|
112
|
+
values: { messages: [{ role: "user", content: "oi" }] },
|
|
113
|
+
config: { configurable: { thread_id: "1" } },
|
|
114
|
+
next: [],
|
|
115
|
+
tasks: [],
|
|
116
|
+
};
|
|
117
|
+
const mockGraph = {
|
|
118
|
+
getState: vi.fn().mockResolvedValue(mockState),
|
|
119
|
+
getStateHistory: vi.fn(async function* () {
|
|
120
|
+
yield mockState;
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
const state = await memory.getState("1", mockGraph as any);
|
|
124
|
+
expect(state).toEqual(mockState);
|
|
125
|
+
expect(mockGraph.getState).toHaveBeenCalledWith({
|
|
126
|
+
configurable: { thread_id: "1" },
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("deve usar getStateHistory quando getState não existe", async () => {
|
|
131
|
+
const memory = new AIMemory({ type: "memory" });
|
|
132
|
+
const mockState = {
|
|
133
|
+
values: { messages: [] },
|
|
134
|
+
config: { configurable: { thread_id: "1" } },
|
|
135
|
+
next: [],
|
|
136
|
+
tasks: [],
|
|
137
|
+
};
|
|
138
|
+
const mockGraph = {
|
|
139
|
+
getStateHistory: vi.fn(async function* () {
|
|
140
|
+
yield mockState;
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
const state = await memory.getState("1", mockGraph as any);
|
|
144
|
+
expect(state).toEqual(mockState);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -103,7 +103,8 @@ describe("AITools", () => {
|
|
|
103
103
|
|
|
104
104
|
it("deve criar múltiplas ferramentas independentes", () => {
|
|
105
105
|
const toolFunction1 = async (input: { x: number }) => input.x * 2;
|
|
106
|
-
const toolFunction2 = async (input: { y: string }) =>
|
|
106
|
+
const toolFunction2 = async (input: { y: string }) =>
|
|
107
|
+
input.y.toUpperCase();
|
|
107
108
|
|
|
108
109
|
const params1: CreateToolParams = {
|
|
109
110
|
toolFunction: toolFunction1,
|