@luanpoppe/ai 1.1.2 → 1.1.4

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.
Files changed (45) hide show
  1. package/dist/@types/image.d.ts +4 -0
  2. package/dist/@types/image.d.ts.map +1 -0
  3. package/dist/@types/image.js +10 -0
  4. package/dist/@types/image.js.map +1 -0
  5. package/dist/ai.d.ts +1 -1
  6. package/dist/ai.d.ts.map +1 -1
  7. package/dist/ai.js +9 -3
  8. package/dist/ai.js.map +1 -1
  9. package/dist/index.d.ts +6 -3
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/langchain/memory.d.ts +96 -0
  14. package/dist/langchain/memory.d.ts.map +1 -0
  15. package/dist/langchain/memory.js +268 -0
  16. package/dist/langchain/memory.js.map +1 -0
  17. package/dist/langchain/messages.d.ts +17 -0
  18. package/dist/langchain/messages.d.ts.map +1 -1
  19. package/dist/langchain/messages.js +21 -0
  20. package/dist/langchain/messages.js.map +1 -1
  21. package/dist/langchain/models.d.ts +4 -0
  22. package/dist/langchain/models.d.ts.map +1 -1
  23. package/dist/langchain/models.js +14 -2
  24. package/dist/langchain/models.js.map +1 -1
  25. package/dist/utils/image-utils.d.ts +8 -0
  26. package/dist/utils/image-utils.d.ts.map +1 -0
  27. package/dist/utils/image-utils.js +91 -0
  28. package/dist/utils/image-utils.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/@types/image.ts +14 -0
  31. package/src/ai.ts +317 -306
  32. package/src/index.ts +7 -2
  33. package/src/langchain/{checkpointers.ts → memory.ts} +48 -22
  34. package/src/langchain/messages.ts +47 -0
  35. package/src/langchain/models.ts +19 -2
  36. package/src/utils/image-utils.ts +113 -0
  37. package/tests/e2e/@fixtures/imagem-teste-bateria.jpg +0 -0
  38. package/tests/e2e/ai-audio.test.ts +3 -3
  39. package/tests/e2e/ai-image.test.ts +173 -0
  40. package/tests/e2e/ai.test.ts +96 -4
  41. package/tests/unit/index.test.ts +98 -3
  42. package/tests/unit/langchain/checkpointers.test.ts +72 -5
  43. package/tests/unit/langchain/image-utils.test.ts +127 -0
  44. package/tests/unit/langchain/messages.test.ts +78 -0
  45. package/tests/unit/langchain/models.test.ts +16 -0
@@ -2,7 +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
+ import { AIMemory } from "../../src/langchain/memory";
6
6
  import z from "zod";
7
7
 
8
8
  // Mock das dependências
@@ -23,8 +23,8 @@ vi.mock("../../src/langchain/models", () => ({
23
23
  },
24
24
  }));
25
25
 
26
- vi.mock("../../src/langchain/checkpointers", async () => {
27
- const actual = await vi.importActual("../../src/langchain/checkpointers");
26
+ vi.mock("../../src/langchain/memory", async () => {
27
+ const actual = await vi.importActual("../../src/langchain/memory");
28
28
  return {
29
29
  ...actual,
30
30
  AIMemory: vi.fn().mockImplementation(function () {
@@ -293,6 +293,101 @@ describe("AI", () => {
293
293
  });
294
294
  });
295
295
 
296
+ it("deve passar reasoningEffort no modelConfig", async () => {
297
+ const mockModel = {} as any;
298
+ const mockMessages = [AIMessages.human("Teste")];
299
+ const mockResponse = {
300
+ messages: [{ content: "Resposta" } as any],
301
+ };
302
+
303
+ vi.mocked(AIModels.gpt).mockReturnValue(mockModel);
304
+ vi.mocked(createAgent).mockReturnValue({
305
+ invoke: vi.fn().mockResolvedValue(mockResponse),
306
+ } as any);
307
+
308
+ await ai.call({
309
+ aiModel: "gpt-5-nano",
310
+ messages: mockMessages,
311
+ modelConfig: {
312
+ reasoningEffort: "medium",
313
+ },
314
+ });
315
+
316
+ expect(AIModels.gpt).toHaveBeenCalledWith({
317
+ model: "gpt-5-nano",
318
+ apiKey: mockTokens.openAIApiKey,
319
+ reasoningEffort: "medium",
320
+ });
321
+ });
322
+
323
+ it("deve exibir warning quando reasoningEffort é passado com modelo Gemini", async () => {
324
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
325
+ const mockModel = {} as any;
326
+ const mockMessages = [AIMessages.human("Teste")];
327
+ const mockResponse = {
328
+ messages: [{ content: "Resposta" } as any],
329
+ };
330
+
331
+ vi.mocked(AIModels.gemini).mockReturnValue(mockModel);
332
+ vi.mocked(createAgent).mockReturnValue({
333
+ invoke: vi.fn().mockResolvedValue(mockResponse),
334
+ } as any);
335
+
336
+ const aiWithGemini = new AI({
337
+ openAIApiKey: "test-key",
338
+ googleGeminiToken: "test-gemini-token",
339
+ });
340
+
341
+ await aiWithGemini.call({
342
+ aiModel: "gemini-2.5-flash",
343
+ messages: mockMessages,
344
+ modelConfig: {
345
+ reasoningEffort: "high",
346
+ },
347
+ });
348
+
349
+ expect(warnSpy).toHaveBeenCalledWith(
350
+ expect.stringContaining('O modelo "gemini-2.5-flash" não suporta reasoningEffort'),
351
+ );
352
+ expect(AIModels.gemini).toHaveBeenCalledWith(
353
+ expect.not.objectContaining({ reasoningEffort: expect.anything() }),
354
+ );
355
+ warnSpy.mockRestore();
356
+ });
357
+
358
+ it("deve exibir warning quando reasoningEffort é passado com OpenRouter não-OpenAI", async () => {
359
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
360
+ const mockModel = {} as any;
361
+ const mockMessages = [AIMessages.human("Teste")];
362
+ const mockResponse = {
363
+ messages: [{ content: "Resposta" } as any],
364
+ };
365
+
366
+ vi.mocked(AIModels.openrouter).mockReturnValue(mockModel);
367
+ vi.mocked(createAgent).mockReturnValue({
368
+ invoke: vi.fn().mockResolvedValue(mockResponse),
369
+ } as any);
370
+
371
+ const aiWithOpenRouter = new AI({
372
+ openRouterApiKey: "test-openrouter-key",
373
+ });
374
+
375
+ await aiWithOpenRouter.call({
376
+ aiModel: "openrouter/google/gemini-2.5-flash",
377
+ messages: mockMessages,
378
+ modelConfig: {
379
+ reasoningEffort: "high",
380
+ },
381
+ });
382
+
383
+ expect(warnSpy).toHaveBeenCalledWith(
384
+ expect.stringContaining(
385
+ 'O modelo "openrouter/google/gemini-2.5-flash" não suporta reasoningEffort',
386
+ ),
387
+ );
388
+ warnSpy.mockRestore();
389
+ });
390
+
296
391
  it("deve lançar erro quando memory está ativo e threadId não é fornecido", async () => {
297
392
  const aiWithMemory = new AI({
298
393
  openAIApiKey: "test-key",
@@ -1,4 +1,4 @@
1
- import { AIMemory } from "../../../src/langchain/checkpointers";
1
+ import { AIMemory } from "../../../src/langchain/memory";
2
2
  import { MemorySaver } from "@langchain/langgraph";
3
3
  import { describe, it, expect, vi } from "vitest";
4
4
 
@@ -97,11 +97,54 @@ describe("AIMemory", () => {
97
97
  });
98
98
  });
99
99
 
100
- it("deve lançar erro quando graph não é passado e agent não foi definido", async () => {
100
+ it("deve usar checkpointer.list quando graph e agent não estão disponíveis", async () => {
101
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
- );
102
+ const mockTuple1 = {
103
+ checkpoint: {
104
+ channel_values: { messages: [{ role: "user", content: "oi" }] },
105
+ ts: "2024-01-01T10:00:00Z",
106
+ },
107
+ config: { configurable: { thread_id: "1" } },
108
+ metadata: {},
109
+ };
110
+ const mockTuple2 = {
111
+ checkpoint: {
112
+ channel_values: {
113
+ messages: [
114
+ { role: "user", content: "oi" },
115
+ { role: "assistant", content: "olá" },
116
+ ],
117
+ },
118
+ ts: "2024-01-01T10:01:00Z",
119
+ },
120
+ config: { configurable: { thread_id: "1" } },
121
+ metadata: {},
122
+ };
123
+ const mockList = vi.fn(async function* () {
124
+ yield mockTuple2;
125
+ yield mockTuple1;
126
+ });
127
+ vi.spyOn(memory, "getCheckpointer").mockResolvedValue({
128
+ list: mockList,
129
+ } as any);
130
+
131
+ const result = await memory.getHistory("1");
132
+
133
+ expect(result.fullHistory).toHaveLength(2);
134
+ expect(result.fullHistory[0].values).toEqual(mockTuple2.checkpoint.channel_values);
135
+ expect(result.fullHistory[1].values).toEqual(mockTuple1.checkpoint.channel_values);
136
+ expect(result.messages).toHaveLength(2);
137
+ expect(result.messages[0]).toEqual({
138
+ role: "human",
139
+ createdAt: "2024-01-01T10:00:00Z",
140
+ content: "oi",
141
+ });
142
+ expect(result.messages[1]).toEqual({
143
+ role: "ai",
144
+ createdAt: "2024-01-01T10:01:00Z",
145
+ content: "olá",
146
+ });
147
+ expect(mockList).toHaveBeenCalledWith({ configurable: { thread_id: "1" } });
105
148
  });
106
149
  });
107
150
 
@@ -143,5 +186,29 @@ describe("AIMemory", () => {
143
186
  const state = await memory.getState("1", mockGraph as any);
144
187
  expect(state).toEqual(mockState);
145
188
  });
189
+
190
+ it("deve usar checkpointer quando graph não está disponível", async () => {
191
+ const memory = new AIMemory({ type: "memory" });
192
+ const mockTuple = {
193
+ checkpoint: {
194
+ channel_values: { messages: [{ role: "user", content: "oi" }] },
195
+ ts: "2024-01-01T10:00:00Z",
196
+ },
197
+ config: { configurable: { thread_id: "1" } },
198
+ metadata: {},
199
+ };
200
+ const mockList = vi.fn(async function* () {
201
+ yield mockTuple;
202
+ });
203
+ vi.spyOn(memory, "getCheckpointer").mockResolvedValue({
204
+ list: mockList,
205
+ } as any);
206
+
207
+ const state = await memory.getState("1");
208
+
209
+ expect(state).not.toBeNull();
210
+ expect(state?.values).toEqual({ messages: [{ role: "user", content: "oi" }] });
211
+ expect(mockList).toHaveBeenCalledWith({ configurable: { thread_id: "1" } });
212
+ });
146
213
  });
147
214
  });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ImageUtils } from "../../../src/utils/image-utils";
3
+
4
+ describe("ImageUtils", () => {
5
+ describe("bufferToBase64", () => {
6
+ it("deve converter Buffer para base64", () => {
7
+ const buffer = Buffer.from("test data");
8
+ const result = ImageUtils.bufferToBase64(buffer);
9
+ expect(result).toBe("dGVzdCBkYXRh");
10
+ });
11
+
12
+ it("deve converter ArrayBuffer para base64", () => {
13
+ const arrayBuffer = new TextEncoder().encode("test data").buffer;
14
+ const result = ImageUtils.bufferToBase64(arrayBuffer);
15
+ expect(result).toBe("dGVzdCBkYXRh");
16
+ });
17
+
18
+ it("deve converter Uint8Array para base64", () => {
19
+ const uint8Array = new TextEncoder().encode("test data");
20
+ const result = ImageUtils.bufferToBase64(uint8Array);
21
+ expect(result).toBe("dGVzdCBkYXRh");
22
+ });
23
+
24
+ it("deve lançar erro para tipo não suportado", () => {
25
+ expect(() => {
26
+ ImageUtils.bufferToBase64("invalid" as any);
27
+ }).toThrow("Tipo de buffer não suportado");
28
+ });
29
+ });
30
+
31
+ describe("detectMimeTypeFromExtension", () => {
32
+ it("deve detectar JPEG pela extensão", () => {
33
+ expect(ImageUtils.detectMimeTypeFromExtension("imagem.jpg")).toBe(
34
+ "image/jpeg"
35
+ );
36
+ expect(ImageUtils.detectMimeTypeFromExtension("foto.jpeg")).toBe(
37
+ "image/jpeg"
38
+ );
39
+ expect(ImageUtils.detectMimeTypeFromExtension("foto.JPG")).toBe(
40
+ "image/jpeg"
41
+ );
42
+ });
43
+
44
+ it("deve detectar PNG pela extensão", () => {
45
+ expect(ImageUtils.detectMimeTypeFromExtension("imagem.png")).toBe(
46
+ "image/png"
47
+ );
48
+ });
49
+
50
+ it("deve detectar GIF pela extensão", () => {
51
+ expect(ImageUtils.detectMimeTypeFromExtension("animacao.gif")).toBe(
52
+ "image/gif"
53
+ );
54
+ });
55
+
56
+ it("deve detectar WebP pela extensão", () => {
57
+ expect(ImageUtils.detectMimeTypeFromExtension("foto.webp")).toBe(
58
+ "image/webp"
59
+ );
60
+ });
61
+
62
+ it("deve retornar null para extensão desconhecida", () => {
63
+ expect(
64
+ ImageUtils.detectMimeTypeFromExtension("arquivo.unknown")
65
+ ).toBeNull();
66
+ });
67
+
68
+ it("deve retornar null quando filename não é fornecido", () => {
69
+ expect(ImageUtils.detectMimeTypeFromExtension(undefined)).toBeNull();
70
+ });
71
+ });
72
+
73
+ describe("detectMimeTypeFromBuffer", () => {
74
+ it("deve detectar JPEG pelos magic bytes", () => {
75
+ const buffer = Buffer.from([0xff, 0xd8, 0xff, 0x00, 0x00]);
76
+ expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/jpeg");
77
+ });
78
+
79
+ it("deve detectar PNG pelos magic bytes", () => {
80
+ const buffer = Buffer.from([
81
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
82
+ 0x00, 0x00, 0x00, 0x00, 0x00,
83
+ ]);
84
+ expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/png");
85
+ });
86
+
87
+ it("deve detectar GIF pelos magic bytes", () => {
88
+ const buffer = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x00, 0x00]);
89
+ expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/gif");
90
+ });
91
+
92
+ it("deve detectar WebP pelos magic bytes", () => {
93
+ const buffer = Buffer.from([
94
+ 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50,
95
+ ]);
96
+ expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBe("image/webp");
97
+ });
98
+
99
+ it("deve retornar null para buffer desconhecido", () => {
100
+ const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
101
+ expect(ImageUtils.detectMimeTypeFromBuffer(buffer)).toBeNull();
102
+ });
103
+ });
104
+
105
+ describe("detectImageMimeType", () => {
106
+ it("deve priorizar extensão sobre magic bytes", () => {
107
+ const buffer = Buffer.from([0xff, 0xd8, 0xff]); // Magic bytes de JPEG
108
+ const result = ImageUtils.detectImageMimeType(buffer, "imagem.png");
109
+ expect(result).toBe("image/png");
110
+ });
111
+
112
+ it("deve usar magic bytes quando extensão não está disponível", () => {
113
+ const buffer = Buffer.from([
114
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00,
115
+ 0x00, 0x00, 0x00, 0x00, 0x00,
116
+ ]);
117
+ const result = ImageUtils.detectImageMimeType(buffer);
118
+ expect(result).toBe("image/png");
119
+ });
120
+
121
+ it("deve usar fallback para JPEG quando não consegue detectar", () => {
122
+ const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]);
123
+ const result = ImageUtils.detectImageMimeType(buffer);
124
+ expect(result).toBe("image/jpeg");
125
+ });
126
+ });
127
+ });
@@ -175,4 +175,82 @@ describe("AIMessages", () => {
175
175
  expect(callArgs.content[0].mime_type).toBe("audio/mp4");
176
176
  });
177
177
  });
178
+
179
+ describe("humanImage", () => {
180
+ it("deve criar uma HumanMessage com conteúdo de imagem e texto", () => {
181
+ const imageBuffer = Buffer.from("fake image data");
182
+ const text = "Descreva esta imagem";
183
+
184
+ const result = AIMessages.humanImage({
185
+ image: {
186
+ buffer: imageBuffer,
187
+ filename: "imagem.jpg",
188
+ },
189
+ text,
190
+ });
191
+
192
+ expect(result).toBeDefined();
193
+ expect(HumanMessage).toHaveBeenCalled();
194
+ const callArgs = (HumanMessage as any).mock.calls[0][0];
195
+ expect(callArgs.content).toBeInstanceOf(Array);
196
+ expect(callArgs.content.length).toBe(2);
197
+ expect(callArgs.content[0].type).toBe("text");
198
+ expect(callArgs.content[0].text).toBe(text);
199
+ expect(callArgs.content[1].type).toBe("image");
200
+ expect(callArgs.content[1].source_type).toBe("base64");
201
+ expect(callArgs.content[1].mime_type).toBe("image/jpeg");
202
+ expect(callArgs.content[1].data).toBeDefined();
203
+ });
204
+
205
+ it("deve criar uma HumanMessage apenas com imagem (sem texto)", () => {
206
+ const imageBuffer = Buffer.from("fake image data");
207
+
208
+ const result = AIMessages.humanImage({
209
+ image: {
210
+ buffer: imageBuffer,
211
+ filename: "imagem.png",
212
+ },
213
+ });
214
+
215
+ expect(result).toBeDefined();
216
+ expect(HumanMessage).toHaveBeenCalled();
217
+ const callArgs = (HumanMessage as any).mock.calls[0][0];
218
+ expect(callArgs.content).toBeInstanceOf(Array);
219
+ expect(callArgs.content.length).toBe(1);
220
+ expect(callArgs.content[0].type).toBe("image");
221
+ expect(callArgs.content[0].source_type).toBe("base64");
222
+ expect(callArgs.content[0].mime_type).toBe("image/png");
223
+ });
224
+
225
+ it("deve detectar MIME type automaticamente pela extensão", () => {
226
+ const imageBuffer = Buffer.from("fake image data");
227
+
228
+ const result = AIMessages.humanImage({
229
+ image: {
230
+ buffer: imageBuffer,
231
+ filename: "foto.webp",
232
+ },
233
+ });
234
+
235
+ expect(result).toBeDefined();
236
+ const callArgs = (HumanMessage as any).mock.calls[0][0];
237
+ expect(callArgs.content[0].mime_type).toBe("image/webp");
238
+ });
239
+
240
+ it("deve usar MIME type fornecido mesmo com extensão", () => {
241
+ const imageBuffer = Buffer.from("fake image data");
242
+
243
+ const result = AIMessages.humanImage({
244
+ image: {
245
+ buffer: imageBuffer,
246
+ mimeType: "image/png",
247
+ filename: "imagem.jpg",
248
+ },
249
+ });
250
+
251
+ expect(result).toBeDefined();
252
+ const callArgs = (HumanMessage as any).mock.calls[0][0];
253
+ expect(callArgs.content[0].mime_type).toBe("image/png");
254
+ });
255
+ });
178
256
  });
@@ -101,6 +101,22 @@ describe("AIModels", () => {
101
101
  "OpenAI API key is not passed in the model parameters"
102
102
  );
103
103
  });
104
+
105
+ it("deve criar uma instância do ChatOpenAI com reasoningEffort", () => {
106
+ const config: LLMModelConfig = {
107
+ model: "gpt-5-nano",
108
+ apiKey: "test-api-key",
109
+ reasoningEffort: "high",
110
+ };
111
+
112
+ AIModels.gpt(config);
113
+
114
+ expect(ChatOpenAI).toHaveBeenCalledWith({
115
+ model: "gpt-5-nano",
116
+ apiKey: "test-api-key",
117
+ modelKwargs: { reasoning_effort: "high" },
118
+ });
119
+ });
104
120
  });
105
121
 
106
122
  describe("gemini", () => {