@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.
- package/dist/ai.d.ts +1 -1
- package/dist/ai.d.ts.map +1 -1
- package/dist/ai.js +3 -3
- package/dist/ai.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/langchain/memory.d.ts +96 -0
- package/dist/langchain/memory.d.ts.map +1 -0
- package/dist/langchain/memory.js +268 -0
- package/dist/langchain/memory.js.map +1 -0
- package/package.json +1 -1
- package/src/ai.ts +306 -306
- package/src/index.ts +2 -2
- package/src/langchain/{checkpointers.ts → memory.ts} +48 -22
- package/tests/e2e/ai.test.ts +52 -0
- package/tests/unit/index.test.ts +3 -3
- package/tests/unit/langchain/checkpointers.test.ts +72 -5
|
@@ -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
|
|
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
|
-
|
|
115
|
-
|
|
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 (
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return
|
|
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
|
|
233
|
-
|
|
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
|
-
|
package/tests/e2e/ai.test.ts
CHANGED
|
@@ -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", () => {
|
package/tests/unit/index.test.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
27
|
-
const actual = await vi.importActual("../../src/langchain/
|
|
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/
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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
|
});
|