@luanpoppe/ai 1.1.2 → 1.1.3

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.
@@ -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,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";
@@ -623,6 +626,55 @@ describe("AI E2E Tests", () => {
623
626
  );
624
627
  },
625
628
  );
629
+
630
+ it(
631
+ "deve retornar histórico via getHistory usando apenas checkpointer - sem graph",
632
+ { timeout },
633
+ async () => {
634
+ if (!openAIApiKey) {
635
+ console.log("OPENAI_API_KEY não está configurada");
636
+ return;
637
+ }
638
+ const memory = new AIMemory({ type: "memory" });
639
+ const checkpointer = await memory.getCheckpointer();
640
+ const model = AIModels.gpt({
641
+ apiKey: openAIApiKey!,
642
+ model: "gpt-5-nano",
643
+ });
644
+ const agent = createAgent({
645
+ model,
646
+ checkpointer,
647
+ systemPrompt: "",
648
+ });
649
+
650
+ const threadId = "e2e-checkpointer-only-thread";
651
+
652
+ await agent.invoke(
653
+ { messages: [AIMessages.human("Responda apenas: recebido.")] },
654
+ { configurable: { thread_id: threadId } },
655
+ );
656
+
657
+ await agent.invoke(
658
+ { messages: [AIMessages.human("O que eu disse antes?")] },
659
+ { configurable: { thread_id: threadId } },
660
+ );
661
+
662
+ const { fullHistory, messages } = await memory.getHistory(threadId);
663
+
664
+ console.log({ messages });
665
+
666
+ expect(fullHistory.length).toBeGreaterThan(0);
667
+ expect(messages.length).toBeGreaterThanOrEqual(2);
668
+
669
+ const allContent = messages.map((m) => m.content).join(" ");
670
+ expect(allContent).toContain("Responda apenas");
671
+ expect(allContent).toContain("O que eu disse");
672
+
673
+ expect(
674
+ messages.every((m) => ["human", "ai", "tool"].includes(m.role)),
675
+ ).toBe(true);
676
+ },
677
+ );
626
678
  });
627
679
 
628
680
  describe("Múltiplas mensagens em conversa", () => {
@@ -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 () {
@@ -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
  });