@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
@@ -84,7 +84,7 @@ export class AIMemory {
84
84
  *
85
85
  * @param threadId - ID da thread/conversa
86
86
  * @param graph - Opcional. Grafo compilado com checkpointer. Se não passado, usa o agent
87
- * definido no construtor ou via setAgent (ex: agent de ai.getRawAgent).
87
+ * definido no construtor ou via setAgent. Se nenhum disponível, usa o checkpointer.
88
88
  * @returns Objeto com fullHistory (checkpoints) e messages (lista com role, createdAt, content)
89
89
  *
90
90
  * @example
@@ -96,6 +96,11 @@ export class AIMemory {
96
96
  * // Passando graph explicitamente
97
97
  * const { fullHistory, messages } = await memory.getHistory("1", agent);
98
98
  *
99
+ * @example
100
+ * // Apenas com checkpointer (sem graph)
101
+ * const memory = new AIMemory({ type: "memory" });
102
+ * const { fullHistory, messages } = await memory.getHistory("1");
103
+ *
99
104
  * @see https://docs.langchain.com/oss/javascript/langgraph/persistence#get-state-history
100
105
  * @see https://docs.langchain.com/oss/javascript/langgraph/add-memory#manage-checkpoints
101
106
  */
@@ -104,21 +109,45 @@ export class AIMemory {
104
109
  graph?: GraphWithStateHistory
105
110
  ): Promise<GetHistoryResult> {
106
111
  const graphToUse = graph ?? this.agent;
107
- if (!graphToUse) {
108
- throw new Error(
109
- "É necessário passar graph em getHistory ou definir o agent no construtor/setAgent do AIMemory."
110
- );
111
- }
112
- const config = { configurable: { thread_id: threadId } };
113
112
  const fullHistory: StateSnapshot[] = [];
114
- for await (const snapshot of graphToUse.getStateHistory(config)) {
115
- fullHistory.push(snapshot);
113
+
114
+ if (graphToUse) {
115
+ const config = { configurable: { thread_id: threadId } };
116
+ for await (const snapshot of graphToUse.getStateHistory(config)) {
117
+ fullHistory.push(snapshot);
118
+ }
119
+ } else {
120
+ const checkpointer = await this.getCheckpointer();
121
+ const config = { configurable: { thread_id: threadId } };
122
+ for await (const tuple of checkpointer.list(config)) {
123
+ fullHistory.push(this.checkpointTupleToSnapshot(tuple));
124
+ }
116
125
  }
117
126
 
118
127
  const messages = this.extractMessagesFromHistory(fullHistory);
119
128
  return { fullHistory, messages };
120
129
  }
121
130
 
131
+ /**
132
+ * Converte CheckpointTuple (do checkpointer.list) em StateSnapshot.
133
+ * Permite obter histórico usando apenas o checkpointer, sem o graph.
134
+ */
135
+ private checkpointTupleToSnapshot(tuple: {
136
+ checkpoint: { channel_values: Record<string, unknown>; ts?: string };
137
+ config?: unknown;
138
+ metadata?: unknown;
139
+ }): StateSnapshot {
140
+ const { checkpoint } = tuple;
141
+ return {
142
+ values: checkpoint.channel_values ?? {},
143
+ createdAt: checkpoint.ts ?? new Date().toISOString(),
144
+ config: tuple.config ?? { configurable: {} },
145
+ metadata: tuple.metadata,
146
+ next: [],
147
+ tasks: [],
148
+ } as StateSnapshot;
149
+ }
150
+
122
151
  /**
123
152
  * Extrai lista de mensagens com role, createdAt e content a partir do histórico.
124
153
  * Percorre os checkpoints do mais antigo ao mais recente para atribuir createdAt correto.
@@ -220,19 +249,17 @@ export class AIMemory {
220
249
  checkpointId?: string
221
250
  ): Promise<StateSnapshot | null> {
222
251
  const graphToUse = graph ?? this.agent;
223
- if (!graphToUse) {
224
- throw new Error(
225
- necessário passar graph em getState ou definir o agent no construtor/setAgent do AIMemory."
226
- );
227
- }
228
- if (!graphToUse.getState) {
229
- const { fullHistory } = await this.getHistory(threadId, graphToUse);
230
- return fullHistory[0] ?? null;
252
+ if (graphToUse?.getState) {
253
+ const config = {
254
+ configurable: {
255
+ thread_id: threadId,
256
+ ...(checkpointId && { checkpoint_id: checkpointId }),
257
+ },
258
+ };
259
+ return (await graphToUse.getState(config)) ?? null;
231
260
  }
232
- const config = {
233
- configurable: { thread_id: threadId, ...(checkpointId && { checkpoint_id: checkpointId }) },
234
- };
235
- return (await graphToUse.getState(config)) ?? null;
261
+ const { fullHistory } = await this.getHistory(threadId, graphToUse ?? undefined);
262
+ return fullHistory[0] ?? null;
236
263
  }
237
264
 
238
265
  private async createCheckpointer(): Promise<BaseCheckpointSaver> {
@@ -328,4 +355,3 @@ export class AIMemory {
328
355
  }
329
356
  }
330
357
  }
331
-
@@ -1,10 +1,29 @@
1
1
  import { SystemMessage, HumanMessage, AIMessage } from "langchain";
2
2
  import { AudioUtils } from "../utils/audio-utils";
3
+ import { ImageUtils } from "../utils/image-utils";
3
4
  import type { AudioBuffer, AudioMimeType } from "../@types/audio";
5
+ import type { ImageBuffer, ImageMimeType } from "../@types/image";
4
6
  import { AIAudioTranscription } from "./audio-transcription";
5
7
 
6
8
  export type MessageInput = SystemMessage | HumanMessage | AIMessage;
7
9
 
10
+ export type ImageContentBlock = {
11
+ type: "image";
12
+ source_type: "base64";
13
+ data: string;
14
+ mime_type: ImageMimeType;
15
+ metadata?: Record<string, unknown>;
16
+ };
17
+
18
+ export type HumanMessageWithImageOptions = {
19
+ image: {
20
+ buffer: ImageBuffer;
21
+ mimeType?: ImageMimeType;
22
+ filename?: string;
23
+ };
24
+ text?: string;
25
+ };
26
+
8
27
  export type AudioContentBlock = {
9
28
  type: "audio";
10
29
  source_type: "base64";
@@ -33,6 +52,34 @@ export class AIMessages {
33
52
  return new HumanMessage(message);
34
53
  }
35
54
 
55
+ static humanImage(options: HumanMessageWithImageOptions): HumanMessage {
56
+ const { image, text } = options;
57
+ const { buffer, mimeType, filename } = image;
58
+
59
+ const base64Data = ImageUtils.bufferToBase64(buffer);
60
+ const detectedMimeType =
61
+ mimeType ?? ImageUtils.detectImageMimeType(buffer, filename);
62
+
63
+ const content: Array<{ type: "text"; text: string } | ImageContentBlock> =
64
+ [];
65
+
66
+ if (text) {
67
+ content.push({ type: "text", text });
68
+ }
69
+
70
+ const imageBlock: ImageContentBlock = {
71
+ type: "image",
72
+ source_type: "base64",
73
+ data: base64Data,
74
+ mime_type: detectedMimeType,
75
+ };
76
+ content.push(imageBlock);
77
+
78
+ return new HumanMessage({
79
+ content: content as any,
80
+ } as any);
81
+ }
82
+
36
83
  static async humanAudio(
37
84
  options: HumanMessageWithAudioOptions
38
85
  ): Promise<HumanMessage> {
@@ -4,16 +4,21 @@ import {
4
4
  } from "@langchain/google-genai";
5
5
  import { ChatOpenAI, ChatOpenAIFields } from "@langchain/openai";
6
6
 
7
+ /** Nível de esforço de raciocínio para modelos OpenAI (o1, gpt-5, etc.). Valores: "low" | "medium" | "high" */
8
+ export type ReasoningEffort = "low" | "medium" | "high";
9
+
7
10
  export type LLMModelConfig = {
8
11
  model: string;
9
12
  apiKey?: string | undefined;
10
13
  maxTokens?: number | undefined;
11
14
  temperature?: number | undefined;
15
+ /** Nível de esforço de raciocínio (modelos OpenAI: o1, gpt-5, etc.) */
16
+ reasoningEffort?: ReasoningEffort | undefined;
12
17
  };
13
18
 
14
19
  export class AIModels {
15
20
  static gpt(params: LLMModelConfig) {
16
- const { model, apiKey, maxTokens, temperature } = params;
21
+ const { model, apiKey, maxTokens, temperature, reasoningEffort } = params;
17
22
  if (!apiKey)
18
23
  throw new Error("OpenAI API key is not passed in the model parameters");
19
24
 
@@ -24,6 +29,12 @@ export class AIModels {
24
29
 
25
30
  if (maxTokens) options.maxTokens = maxTokens;
26
31
  if (temperature) options.temperature = temperature;
32
+ if (reasoningEffort) {
33
+ options.modelKwargs = {
34
+ ...(options.modelKwargs ?? {}),
35
+ reasoning_effort: reasoningEffort,
36
+ };
37
+ }
27
38
 
28
39
  return new ChatOpenAI(options);
29
40
  }
@@ -48,7 +59,7 @@ export class AIModels {
48
59
  }
49
60
 
50
61
  static openrouter(params: LLMModelConfig) {
51
- const { apiKey, maxTokens, model, temperature } = params;
62
+ const { apiKey, maxTokens, model, temperature, reasoningEffort } = params;
52
63
 
53
64
  if (!apiKey)
54
65
  throw new Error(
@@ -65,6 +76,12 @@ export class AIModels {
65
76
 
66
77
  if (maxTokens) options.maxTokens = maxTokens;
67
78
  if (temperature) options.temperature = temperature;
79
+ if (reasoningEffort) {
80
+ options.modelKwargs = {
81
+ ...(options.modelKwargs ?? {}),
82
+ reasoning_effort: reasoningEffort,
83
+ };
84
+ }
68
85
 
69
86
  return new ChatOpenAI(options);
70
87
  }
@@ -0,0 +1,113 @@
1
+ import type { ImageBuffer, ImageMimeType } from "../@types/image";
2
+
3
+ export class ImageUtils {
4
+ static bufferToBase64(buffer: ImageBuffer): string {
5
+ if (buffer instanceof Buffer) {
6
+ return buffer.toString("base64");
7
+ }
8
+
9
+ if (buffer instanceof ArrayBuffer) {
10
+ const uint8Array = new Uint8Array(buffer);
11
+ return Buffer.from(uint8Array).toString("base64");
12
+ }
13
+
14
+ if (buffer instanceof Uint8Array) {
15
+ return Buffer.from(buffer).toString("base64");
16
+ }
17
+
18
+ throw new Error("Tipo de buffer não suportado");
19
+ }
20
+
21
+ static detectMimeTypeFromExtension(filename?: string): ImageMimeType | null {
22
+ if (!filename) {
23
+ return null;
24
+ }
25
+
26
+ const extension = filename.toLowerCase().split(".").pop();
27
+
28
+ const mimeTypeMap: Record<string, ImageMimeType> = {
29
+ jpg: "image/jpeg",
30
+ jpeg: "image/jpeg",
31
+ png: "image/png",
32
+ gif: "image/gif",
33
+ webp: "image/webp",
34
+ };
35
+
36
+ return extension ? mimeTypeMap[extension] ?? null : null;
37
+ }
38
+
39
+ static detectMimeTypeFromBuffer(buffer: ImageBuffer): ImageMimeType | null {
40
+ let bytes: Uint8Array;
41
+
42
+ if (buffer instanceof Buffer) {
43
+ bytes = new Uint8Array(buffer.slice(0, 16));
44
+ } else if (buffer instanceof ArrayBuffer) {
45
+ bytes = new Uint8Array(buffer.slice(0, 16));
46
+ } else {
47
+ bytes = buffer.slice(0, 16);
48
+ }
49
+
50
+ // JPEG: FF D8 FF
51
+ if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
52
+ return "image/jpeg";
53
+ }
54
+
55
+ // PNG: 89 50 4E 47 0D 0A 1A 0A
56
+ if (
57
+ bytes[0] === 0x89 &&
58
+ bytes[1] === 0x50 &&
59
+ bytes[2] === 0x4e &&
60
+ bytes[3] === 0x47 &&
61
+ bytes[4] === 0x0d &&
62
+ bytes[5] === 0x0a &&
63
+ bytes[6] === 0x1a &&
64
+ bytes[7] === 0x0a
65
+ ) {
66
+ return "image/png";
67
+ }
68
+
69
+ // GIF: 47 49 46 38 (GIF8)
70
+ if (
71
+ bytes[0] === 0x47 &&
72
+ bytes[1] === 0x49 &&
73
+ bytes[2] === 0x46 &&
74
+ bytes[3] === 0x38
75
+ ) {
76
+ return "image/gif";
77
+ }
78
+
79
+ // WebP: 52 49 46 46 (RIFF) ... 57 45 42 50 (WEBP)
80
+ if (
81
+ bytes[0] === 0x52 &&
82
+ bytes[1] === 0x49 &&
83
+ bytes[2] === 0x46 &&
84
+ bytes[3] === 0x46 &&
85
+ bytes[8] === 0x57 &&
86
+ bytes[9] === 0x45 &&
87
+ bytes[10] === 0x42 &&
88
+ bytes[11] === 0x50
89
+ ) {
90
+ return "image/webp";
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ static detectImageMimeType(
97
+ buffer: ImageBuffer,
98
+ filename?: string
99
+ ): ImageMimeType {
100
+ const mimeFromExtension =
101
+ ImageUtils.detectMimeTypeFromExtension(filename);
102
+ if (mimeFromExtension) {
103
+ return mimeFromExtension;
104
+ }
105
+
106
+ const mimeFromBuffer = ImageUtils.detectMimeTypeFromBuffer(buffer);
107
+ if (mimeFromBuffer) {
108
+ return mimeFromBuffer;
109
+ }
110
+
111
+ return "image/jpeg";
112
+ }
113
+ }
@@ -58,7 +58,7 @@ describe("AI Audio E2E Tests", () => {
58
58
  }
59
59
  );
60
60
 
61
- it(
61
+ it.skip(
62
62
  "deve processar áudio diretamente com Gemini (multimodal)",
63
63
  { timeout },
64
64
  async () => {
@@ -132,7 +132,7 @@ describe("AI Audio E2E Tests", () => {
132
132
  }
133
133
  );
134
134
 
135
- it(
135
+ it.skip(
136
136
  "deve processar áudio com OpenRouter usando Gemini (multimodal)",
137
137
  { timeout },
138
138
  async () => {
@@ -168,7 +168,7 @@ describe("AI Audio E2E Tests", () => {
168
168
  }
169
169
  );
170
170
 
171
- it(
171
+ it.skip(
172
172
  "deve processar áudio com OpenRouter usando GPT (transcrição prévia com Whisper)",
173
173
  { timeout },
174
174
  async () => {
@@ -0,0 +1,173 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { AI } from "../../src/index";
4
+ import { AIMessages } from "../../src/langchain/messages";
5
+ import "dotenv/config";
6
+
7
+ const IMAGE_FIXTURE_PATH = path.join(
8
+ process.cwd(),
9
+ "tests",
10
+ "e2e",
11
+ "@fixtures",
12
+ "imagem-teste-bateria.jpg"
13
+ );
14
+
15
+ const PROMPT =
16
+ "Descreva brevemente o que há nesta imagem. Responda em uma frase.";
17
+
18
+ const EXPECTED_TERMS = [
19
+ "bateria",
20
+ "baterista",
21
+ "pratos",
22
+ "tambor",
23
+ "drum",
24
+ "cymbal",
25
+ "palco",
26
+ "stage",
27
+ ];
28
+
29
+ function responseRelatesToImage(text: string): boolean {
30
+ const lower = text.toLowerCase();
31
+ return EXPECTED_TERMS.some((term) => lower.includes(term));
32
+ }
33
+
34
+ describe("AI Image E2E Tests", () => {
35
+ const openAIApiKey = process.env.OPENAI_API_KEY;
36
+ const googleGeminiToken = process.env.GOOGLE_GEMINI_TOKEN;
37
+ const openRouterApiKey = process.env.OPENROUTER_API_KEY;
38
+
39
+ const timeout = 180 * 1000;
40
+
41
+ describe("Suporte a imagens", () => {
42
+ it(
43
+ "deve processar imagem com OpenAI (gpt-4o)",
44
+ { timeout },
45
+ async () => {
46
+ if (!openAIApiKey) {
47
+ console.log("OPENAI_API_KEY não está configurada");
48
+ return;
49
+ }
50
+ const ai = new AI({
51
+ openAIApiKey: openAIApiKey!,
52
+ });
53
+
54
+ const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
55
+ const message = AIMessages.humanImage({
56
+ image: {
57
+ buffer: imageBuffer,
58
+ filename: "imagem-teste-bateria.jpg",
59
+ },
60
+ text: PROMPT,
61
+ });
62
+
63
+ const result = await ai.call({
64
+ aiModel: "gpt-4o",
65
+ messages: [message],
66
+ });
67
+
68
+ expect(result.text).toBeDefined();
69
+ expect(result.text.length).toBeGreaterThan(0);
70
+ expect(result.text).not.toContain("Model call failed");
71
+ expect(result.text).not.toContain("Invalid");
72
+ expect(responseRelatesToImage(result.text)).toBe(true);
73
+ }
74
+ );
75
+
76
+ it.skip(
77
+ "deve processar imagem com Gemini (gemini-2.5-flash)",
78
+ { timeout },
79
+ async () => {
80
+ if (!googleGeminiToken) {
81
+ console.log("GOOGLE_GEMINI_TOKEN não está configurada");
82
+ return;
83
+ }
84
+ const ai = new AI({
85
+ googleGeminiToken: googleGeminiToken!,
86
+ });
87
+
88
+ const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
89
+ const message = AIMessages.humanImage({
90
+ image: {
91
+ buffer: imageBuffer,
92
+ filename: "imagem-teste-bateria.jpg",
93
+ },
94
+ text: PROMPT,
95
+ });
96
+
97
+ const result = await ai.call({
98
+ aiModel: "gemini-2.5-flash",
99
+ messages: [message],
100
+ });
101
+
102
+ expect(result.text).toBeDefined();
103
+ expect(result.text.length).toBeGreaterThan(0);
104
+ expect(responseRelatesToImage(result.text)).toBe(true);
105
+ }
106
+ );
107
+
108
+ it.skip(
109
+ "deve processar imagem com OpenRouter (openrouter/openai/gpt-4o)",
110
+ { timeout },
111
+ async () => {
112
+ if (!openRouterApiKey) {
113
+ console.log("OPENROUTER_API_KEY não está configurada");
114
+ return;
115
+ }
116
+ const ai = new AI({
117
+ openRouterApiKey: openRouterApiKey!,
118
+ });
119
+
120
+ const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
121
+ const message = AIMessages.humanImage({
122
+ image: {
123
+ buffer: imageBuffer,
124
+ filename: "imagem-teste-bateria.jpg",
125
+ },
126
+ text: PROMPT,
127
+ });
128
+
129
+ const result = await ai.call({
130
+ aiModel: "openrouter/openai/gpt-4o",
131
+ messages: [message],
132
+ });
133
+
134
+ expect(result.text).toBeDefined();
135
+ expect(result.text.length).toBeGreaterThan(0);
136
+ expect(result.text).not.toContain("Model call failed");
137
+ expect(responseRelatesToImage(result.text)).toBe(true);
138
+ }
139
+ );
140
+
141
+ it.skip(
142
+ "deve processar imagem com OpenRouter (openrouter/google/gemini-2.5-flash)",
143
+ { timeout },
144
+ async () => {
145
+ if (!openRouterApiKey) {
146
+ console.log("OPENROUTER_API_KEY não está configurada");
147
+ return;
148
+ }
149
+ const ai = new AI({
150
+ openRouterApiKey: openRouterApiKey!,
151
+ });
152
+
153
+ const imageBuffer = fs.readFileSync(IMAGE_FIXTURE_PATH);
154
+ const message = AIMessages.humanImage({
155
+ image: {
156
+ buffer: imageBuffer,
157
+ filename: "imagem-teste-bateria.jpg",
158
+ },
159
+ text: PROMPT,
160
+ });
161
+
162
+ const result = await ai.call({
163
+ aiModel: "openrouter/google/gemini-2.5-flash",
164
+ messages: [message],
165
+ });
166
+
167
+ expect(result.text).toBeDefined();
168
+ expect(result.text.length).toBeGreaterThan(0);
169
+ expect(responseRelatesToImage(result.text)).toBe(true);
170
+ }
171
+ );
172
+ });
173
+ });
@@ -1,4 +1,7 @@
1
+ import { createAgent } from "langchain";
1
2
  import { AI } from "../../src/index";
3
+ import { AIMemory } from "../../src/langchain/memory";
4
+ import { AIModels } from "../../src/langchain/models";
2
5
  import { AIMessages } from "../../src/langchain/messages";
3
6
  import z from "zod";
4
7
  import "dotenv/config";
@@ -42,7 +45,7 @@ describe("AI E2E Tests", () => {
42
45
  },
43
46
  );
44
47
 
45
- it(
48
+ it.skip(
46
49
  "deve fazer uma chamada real para Gemini e retornar resposta",
47
50
  { timeout },
48
51
  async () => {
@@ -136,7 +139,7 @@ describe("AI E2E Tests", () => {
136
139
  },
137
140
  );
138
141
 
139
- it(
142
+ it.skip(
140
143
  "deve criar múltiplas mensagens e fazer chamada",
141
144
  { timeout },
142
145
  async () => {
@@ -345,7 +348,7 @@ describe("AI E2E Tests", () => {
345
348
  },
346
349
  );
347
350
 
348
- it(
351
+ it.skip(
349
352
  "deve retornar resposta estruturada com Gemini",
350
353
  { timeout },
351
354
  async () => {
@@ -481,8 +484,9 @@ describe("AI E2E Tests", () => {
481
484
  AIMessages.human("Explique o que é TypeScript em uma frase curta."),
482
485
  ];
483
486
 
487
+ // gpt-4o suporta temperature; gpt-5-nano (reasoning) só aceita default
484
488
  const result = await ai.call({
485
- aiModel: "gpt-5-nano",
489
+ aiModel: "gpt-4o",
486
490
  messages,
487
491
  modelConfig: {
488
492
  maxTokens: 50,
@@ -496,6 +500,45 @@ describe("AI E2E Tests", () => {
496
500
  },
497
501
  );
498
502
 
503
+ // Níveis suportados pela API OpenAI (o1, gpt-5-nano, etc.): low, medium, high.
504
+ // Modelos mais recentes (gpt-5.2) podem suportar também: none, minimal, xhigh.
505
+ it.each([
506
+ { reasoningEffort: "low" as const },
507
+ { reasoningEffort: "medium" as const },
508
+ { reasoningEffort: "high" as const },
509
+ ])(
510
+ "deve usar reasoningEffort=$reasoningEffort em chamada real ao GPT",
511
+ { timeout },
512
+ async ({ reasoningEffort }) => {
513
+ if (!openAIApiKey) {
514
+ console.log("OPENAI_API_KEY não está configurada");
515
+ return;
516
+ }
517
+ const ai = new AI({
518
+ openAIApiKey: openAIApiKey!,
519
+ });
520
+
521
+ const messages = [
522
+ AIMessages.human(
523
+ "Responda apenas com a palavra 'OK' (modelo de reasoning).",
524
+ ),
525
+ ];
526
+
527
+ const result = await ai.call({
528
+ aiModel: "gpt-5-nano",
529
+ messages,
530
+ modelConfig: {
531
+ reasoningEffort,
532
+ },
533
+ });
534
+
535
+ expect(result.text).toBeDefined();
536
+ expect(result.text.length).toBeGreaterThan(0);
537
+ expect(result.messages).toBeDefined();
538
+ expect(result.messages.length).toBeGreaterThan(0);
539
+ },
540
+ );
541
+
499
542
  it(
500
543
  "deve usar maxTokens e temperature corretamente com OpenRouter",
501
544
  { timeout },
@@ -623,6 +666,55 @@ describe("AI E2E Tests", () => {
623
666
  );
624
667
  },
625
668
  );
669
+
670
+ it(
671
+ "deve retornar histórico via getHistory usando apenas checkpointer - sem graph",
672
+ { timeout },
673
+ async () => {
674
+ if (!openAIApiKey) {
675
+ console.log("OPENAI_API_KEY não está configurada");
676
+ return;
677
+ }
678
+ const memory = new AIMemory({ type: "memory" });
679
+ const checkpointer = await memory.getCheckpointer();
680
+ const model = AIModels.gpt({
681
+ apiKey: openAIApiKey!,
682
+ model: "gpt-5-nano",
683
+ });
684
+ const agent = createAgent({
685
+ model,
686
+ checkpointer,
687
+ systemPrompt: "",
688
+ });
689
+
690
+ const threadId = "e2e-checkpointer-only-thread";
691
+
692
+ await agent.invoke(
693
+ { messages: [AIMessages.human("Responda apenas: recebido.")] },
694
+ { configurable: { thread_id: threadId } },
695
+ );
696
+
697
+ await agent.invoke(
698
+ { messages: [AIMessages.human("O que eu disse antes?")] },
699
+ { configurable: { thread_id: threadId } },
700
+ );
701
+
702
+ const { fullHistory, messages } = await memory.getHistory(threadId);
703
+
704
+ console.log({ messages });
705
+
706
+ expect(fullHistory.length).toBeGreaterThan(0);
707
+ expect(messages.length).toBeGreaterThanOrEqual(2);
708
+
709
+ const allContent = messages.map((m) => m.content).join(" ");
710
+ expect(allContent).toContain("Responda apenas");
711
+ expect(allContent).toContain("O que eu disse");
712
+
713
+ expect(
714
+ messages.every((m) => ["human", "ai", "tool"].includes(m.role)),
715
+ ).toBe(true);
716
+ },
717
+ );
626
718
  });
627
719
 
628
720
  describe("Múltiplas mensagens em conversa", () => {