@luanpoppe/ai 1.1.3 → 1.1.5
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/image.d.ts +4 -0
- package/dist/@types/image.d.ts.map +1 -0
- package/dist/@types/image.js +10 -0
- package/dist/@types/image.js.map +1 -0
- package/dist/@types/model-names.d.ts +16 -4
- package/dist/@types/model-names.d.ts.map +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +6 -0
- package/dist/ai.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/langchain/messages.d.ts +17 -0
- package/dist/langchain/messages.d.ts.map +1 -1
- package/dist/langchain/messages.js +21 -0
- package/dist/langchain/messages.js.map +1 -1
- package/dist/langchain/models.d.ts +4 -0
- package/dist/langchain/models.d.ts.map +1 -1
- package/dist/langchain/models.js +14 -2
- package/dist/langchain/models.js.map +1 -1
- package/dist/utils/image-utils.d.ts +8 -0
- package/dist/utils/image-utils.d.ts.map +1 -0
- package/dist/utils/image-utils.js +91 -0
- package/dist/utils/image-utils.js.map +1 -0
- package/package.json +1 -1
- package/src/@types/image.ts +14 -0
- package/src/@types/model-names.ts +92 -5
- package/src/ai.ts +11 -0
- package/src/index.ts +6 -0
- package/src/langchain/messages.ts +47 -0
- package/src/langchain/models.ts +19 -2
- package/src/utils/image-utils.ts +113 -0
- package/tests/e2e/@fixtures/imagem-teste-bateria.jpg +0 -0
- package/tests/e2e/ai-audio.test.ts +3 -3
- package/tests/e2e/ai-image.test.ts +173 -0
- package/tests/e2e/ai.test.ts +44 -4
- package/tests/unit/index.test.ts +95 -0
- package/tests/unit/langchain/image-utils.test.ts +127 -0
- package/tests/unit/langchain/messages.test.ts +78 -0
- package/tests/unit/langchain/models.test.ts +16 -0
|
@@ -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", () => {
|