@batonfx/memory 0.3.7 → 0.4.1
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/README.md +5 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -33110
- package/dist/semantic-recall.d.ts +15 -0
- package/dist/semantic-recall.js +90 -0
- package/dist/vector-store.d.ts +51 -0
- package/dist/vector-store.js +77 -0
- package/dist/working-memory.d.ts +19 -0
- package/dist/working-memory.js +143 -0
- package/package.json +29 -4
- package/.turbo/turbo-build.log +0 -5
- package/src/index.ts +0 -29
- package/src/semantic-recall.ts +0 -123
- package/src/vector-store.ts +0 -132
- package/src/working-memory.ts +0 -208
- package/test/index.test.ts +0 -55
- package/test/semantic-recall.test.ts +0 -154
- package/test/vector-store.test.ts +0 -121
- package/test/working-memory.test.ts +0 -155
- package/tsconfig.json +0 -7
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { EmbeddingModel } from "effect/unstable/ai";
|
|
3
|
+
import { Memory } from "@batonfx/core";
|
|
4
|
+
import { VectorStore } from "./vector-store.js";
|
|
5
|
+
/** @experimental */
|
|
6
|
+
export interface Options {
|
|
7
|
+
readonly limit?: number;
|
|
8
|
+
readonly minScore?: number;
|
|
9
|
+
}
|
|
10
|
+
/** @experimental */
|
|
11
|
+
export declare const make: (options?: Options) => Effect.Effect<Memory.Interface, never, VectorStore | EmbeddingModel.EmbeddingModel>;
|
|
12
|
+
/** @experimental */
|
|
13
|
+
export declare const makeSemanticRecall: (options?: Options) => Effect.Effect<Memory.Interface, never, VectorStore | EmbeddingModel.EmbeddingModel>;
|
|
14
|
+
/** @experimental */
|
|
15
|
+
export declare const layer: (options?: Options) => Layer.Layer<Memory.Memory, never, VectorStore | EmbeddingModel.EmbeddingModel>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect, Layer, Ref } from "effect";
|
|
2
|
+
import { EmbeddingModel, Prompt } from "effect/unstable/ai";
|
|
3
|
+
import { Memory } from "@batonfx/core";
|
|
4
|
+
import { VectorStore } from "./vector-store.js";
|
|
5
|
+
const errorMessage = (error) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error));
|
|
6
|
+
const memoryError = (error) => new Memory.MemoryError({ message: errorMessage(error) });
|
|
7
|
+
const textPart = (text) => Prompt.makePart("text", { text });
|
|
8
|
+
const textFromParts = (parts) => parts
|
|
9
|
+
.filter((part) => part.type === "text")
|
|
10
|
+
.map((part) => part.text)
|
|
11
|
+
.join("");
|
|
12
|
+
const userText = (prompt) => prompt.content
|
|
13
|
+
.filter((message) => message.role === "user")
|
|
14
|
+
.map((message) => textFromParts(message.content))
|
|
15
|
+
.filter((text) => text.length > 0)
|
|
16
|
+
.join("\n\n");
|
|
17
|
+
const finalExchangeText = (prompt) => {
|
|
18
|
+
let assistant;
|
|
19
|
+
let assistantIndex = -1;
|
|
20
|
+
for (let index = prompt.content.length - 1; index >= 0; index -= 1) {
|
|
21
|
+
const message = prompt.content[index];
|
|
22
|
+
if (message?.role !== "assistant")
|
|
23
|
+
continue;
|
|
24
|
+
const text = textFromParts(message.content).trim();
|
|
25
|
+
if (text.length === 0)
|
|
26
|
+
continue;
|
|
27
|
+
assistant = text;
|
|
28
|
+
assistantIndex = index;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
if (assistant === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
for (let index = assistantIndex - 1; index >= 0; index -= 1) {
|
|
34
|
+
const message = prompt.content[index];
|
|
35
|
+
if (message?.role !== "user")
|
|
36
|
+
continue;
|
|
37
|
+
const text = textFromParts(message.content).trim();
|
|
38
|
+
if (text.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
return `User: ${text}\nAssistant: ${assistant}`;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
const itemFromMatch = (match) => ({
|
|
45
|
+
id: match.document.id,
|
|
46
|
+
parts: [textPart(match.document.text)],
|
|
47
|
+
metadata: { ...match.document.metadata, score: match.score },
|
|
48
|
+
});
|
|
49
|
+
/** @experimental */
|
|
50
|
+
export const make = (options = {}) => Effect.gen(function* () {
|
|
51
|
+
const store = yield* VectorStore;
|
|
52
|
+
const embeddingModel = yield* EmbeddingModel.EmbeddingModel;
|
|
53
|
+
const counter = yield* Ref.make(0);
|
|
54
|
+
const limit = options.limit ?? 5;
|
|
55
|
+
return {
|
|
56
|
+
recall: (input) => {
|
|
57
|
+
const text = userText(input.prompt);
|
|
58
|
+
if (text.length === 0)
|
|
59
|
+
return Effect.succeed([]);
|
|
60
|
+
return embeddingModel.embed(text).pipe(Effect.mapError(memoryError), Effect.flatMap((embedding) => store
|
|
61
|
+
.query({
|
|
62
|
+
key: input.key,
|
|
63
|
+
embedding: embedding.vector,
|
|
64
|
+
limit,
|
|
65
|
+
...(options.minScore === undefined ? {} : { minScore: options.minScore }),
|
|
66
|
+
})
|
|
67
|
+
.pipe(Effect.mapError(memoryError))), Effect.map((matches) => matches.map(itemFromMatch)));
|
|
68
|
+
},
|
|
69
|
+
remember: (input) => {
|
|
70
|
+
if (!input.terminal)
|
|
71
|
+
return Effect.void;
|
|
72
|
+
const text = finalExchangeText(input.transcript);
|
|
73
|
+
if (text === undefined)
|
|
74
|
+
return Effect.void;
|
|
75
|
+
return embeddingModel.embed(text).pipe(Effect.mapError(memoryError), Effect.flatMap((embedding) => Ref.modify(counter, (current) => [`semantic-${current + 1}`, current + 1]).pipe(Effect.flatMap((id) => store.upsert([
|
|
76
|
+
{
|
|
77
|
+
id,
|
|
78
|
+
key: input.key,
|
|
79
|
+
text,
|
|
80
|
+
embedding: embedding.vector,
|
|
81
|
+
},
|
|
82
|
+
])))), Effect.mapError(memoryError));
|
|
83
|
+
},
|
|
84
|
+
forget: (input) => store.delete(input).pipe(Effect.mapError(memoryError)),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
/** @experimental */
|
|
88
|
+
export const makeSemanticRecall = make;
|
|
89
|
+
/** @experimental */
|
|
90
|
+
export const layer = (options = {}) => Layer.effect(Memory.Memory, make(options).pipe(Effect.map(Memory.Memory.of)));
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
2
|
+
import { Memory } from "@batonfx/core";
|
|
3
|
+
/** @experimental */
|
|
4
|
+
export interface Document {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly key: Memory.Key;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly metadata?: Memory.Metadata;
|
|
9
|
+
}
|
|
10
|
+
/** @experimental */
|
|
11
|
+
export interface Embedded extends Document {
|
|
12
|
+
readonly embedding: ReadonlyArray<number>;
|
|
13
|
+
}
|
|
14
|
+
/** @experimental */
|
|
15
|
+
export interface Match {
|
|
16
|
+
readonly document: Embedded;
|
|
17
|
+
readonly score: number;
|
|
18
|
+
}
|
|
19
|
+
/** @experimental */
|
|
20
|
+
export interface Query {
|
|
21
|
+
readonly key: Memory.Key;
|
|
22
|
+
readonly embedding: ReadonlyArray<number>;
|
|
23
|
+
readonly limit: number;
|
|
24
|
+
readonly minScore?: number;
|
|
25
|
+
}
|
|
26
|
+
/** @experimental */
|
|
27
|
+
export interface DeleteInput {
|
|
28
|
+
readonly key: Memory.Key;
|
|
29
|
+
readonly id?: string | undefined;
|
|
30
|
+
}
|
|
31
|
+
declare const VectorStoreError_base: Schema.Class<VectorStoreError, Schema.TaggedStruct<"@batonfx/memory/VectorStoreError", {
|
|
32
|
+
readonly message: Schema.String;
|
|
33
|
+
}>, import("effect/Cause").YieldableError>;
|
|
34
|
+
/** @experimental */
|
|
35
|
+
export declare class VectorStoreError extends VectorStoreError_base {
|
|
36
|
+
}
|
|
37
|
+
/** @experimental */
|
|
38
|
+
export interface Interface {
|
|
39
|
+
readonly upsert: (documents: ReadonlyArray<Embedded>) => Effect.Effect<void, VectorStoreError>;
|
|
40
|
+
readonly query: (query: Query) => Effect.Effect<ReadonlyArray<Match>, VectorStoreError>;
|
|
41
|
+
readonly delete: (input: DeleteInput) => Effect.Effect<void, VectorStoreError>;
|
|
42
|
+
}
|
|
43
|
+
declare const VectorStore_base: Context.ServiceClass<VectorStore, "@batonfx/memory/VectorStore", Interface>;
|
|
44
|
+
/** @experimental */
|
|
45
|
+
export declare class VectorStore extends VectorStore_base {
|
|
46
|
+
}
|
|
47
|
+
/** @experimental */
|
|
48
|
+
export declare const memoryLayer: Layer.Layer<VectorStore>;
|
|
49
|
+
/** @experimental */
|
|
50
|
+
export declare const testLayer: (implementation: Interface) => Layer.Layer<VectorStore>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Context, Effect, HashMap, Layer, Ref, Schema } from "effect";
|
|
2
|
+
import { Memory } from "@batonfx/core";
|
|
3
|
+
/** @experimental */
|
|
4
|
+
export class VectorStoreError extends Schema.TaggedErrorClass()("@batonfx/memory/VectorStoreError", {
|
|
5
|
+
message: Schema.String,
|
|
6
|
+
}) {
|
|
7
|
+
}
|
|
8
|
+
/** @experimental */
|
|
9
|
+
export class VectorStore extends Context.Service()("@batonfx/memory/VectorStore") {
|
|
10
|
+
}
|
|
11
|
+
const storageKey = (key, id) => JSON.stringify([key.agent, key.subject, id]);
|
|
12
|
+
const sameKey = (left, right) => left.agent === right.agent && left.subject === right.subject;
|
|
13
|
+
const validateVector = (label, vector) => {
|
|
14
|
+
const invalid = vector.find((value) => !Number.isFinite(value));
|
|
15
|
+
return invalid === undefined
|
|
16
|
+
? Effect.void
|
|
17
|
+
: Effect.fail(new VectorStoreError({ message: `${label} contains a non-finite value` }));
|
|
18
|
+
};
|
|
19
|
+
const cosine = (left, right) => {
|
|
20
|
+
let dot = 0;
|
|
21
|
+
let leftMagnitude = 0;
|
|
22
|
+
let rightMagnitude = 0;
|
|
23
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
24
|
+
const leftValue = left[index] ?? 0;
|
|
25
|
+
const rightValue = right[index] ?? 0;
|
|
26
|
+
dot += leftValue * rightValue;
|
|
27
|
+
leftMagnitude += leftValue * leftValue;
|
|
28
|
+
rightMagnitude += rightValue * rightValue;
|
|
29
|
+
}
|
|
30
|
+
if (leftMagnitude === 0 || rightMagnitude === 0)
|
|
31
|
+
return 0;
|
|
32
|
+
return dot / (Math.sqrt(leftMagnitude) * Math.sqrt(rightMagnitude));
|
|
33
|
+
};
|
|
34
|
+
const make = Ref.make(HashMap.empty()).pipe(Effect.map((documents) => ({
|
|
35
|
+
upsert: (nextDocuments) => Effect.gen(function* () {
|
|
36
|
+
for (const document of nextDocuments) {
|
|
37
|
+
yield* validateVector(`document ${document.id} embedding`, document.embedding);
|
|
38
|
+
}
|
|
39
|
+
yield* Ref.update(documents, (current) => {
|
|
40
|
+
let next = current;
|
|
41
|
+
for (const document of nextDocuments) {
|
|
42
|
+
next = HashMap.set(next, storageKey(document.key, document.id), document);
|
|
43
|
+
}
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
46
|
+
}),
|
|
47
|
+
query: (input) => Effect.gen(function* () {
|
|
48
|
+
if (input.limit <= 0)
|
|
49
|
+
return [];
|
|
50
|
+
yield* validateVector("query embedding", input.embedding);
|
|
51
|
+
const current = yield* Ref.get(documents);
|
|
52
|
+
const matches = [];
|
|
53
|
+
for (const [, document] of current) {
|
|
54
|
+
if (!sameKey(document.key, input.key))
|
|
55
|
+
continue;
|
|
56
|
+
if (document.embedding.length !== input.embedding.length) {
|
|
57
|
+
return yield* Effect.fail(new VectorStoreError({
|
|
58
|
+
message: `document ${document.id} embedding dimension ${document.embedding.length} does not match query dimension ${input.embedding.length}`,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
const score = cosine(document.embedding, input.embedding);
|
|
62
|
+
if (input.minScore !== undefined && score < input.minScore)
|
|
63
|
+
continue;
|
|
64
|
+
matches.push({ document, score });
|
|
65
|
+
}
|
|
66
|
+
return matches.toSorted((left, right) => right.score - left.score).slice(0, input.limit);
|
|
67
|
+
}),
|
|
68
|
+
delete: (input) => Ref.update(documents, (current) => HashMap.filter(current, (document) => {
|
|
69
|
+
if (!sameKey(document.key, input.key))
|
|
70
|
+
return true;
|
|
71
|
+
return input.id === undefined ? false : document.id !== input.id;
|
|
72
|
+
})),
|
|
73
|
+
})));
|
|
74
|
+
/** @experimental */
|
|
75
|
+
export const memoryLayer = Layer.effect(VectorStore, make.pipe(Effect.map(VectorStore.of)));
|
|
76
|
+
/** @experimental */
|
|
77
|
+
export const testLayer = (implementation) => Layer.succeed(VectorStore, VectorStore.of(implementation));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect";
|
|
2
|
+
import { LanguageModel } from "effect/unstable/ai";
|
|
3
|
+
import { Memory } from "@batonfx/core";
|
|
4
|
+
/** @experimental */
|
|
5
|
+
export interface SummarizeOptions {
|
|
6
|
+
readonly model: Layer.Layer<LanguageModel.LanguageModel>;
|
|
7
|
+
readonly prompt?: string;
|
|
8
|
+
}
|
|
9
|
+
/** @experimental */
|
|
10
|
+
export interface Options {
|
|
11
|
+
readonly maxMessages?: number;
|
|
12
|
+
readonly summarize?: SummarizeOptions;
|
|
13
|
+
}
|
|
14
|
+
/** @experimental */
|
|
15
|
+
export declare const make: (options?: Options) => Effect.Effect<Memory.Interface>;
|
|
16
|
+
/** @experimental */
|
|
17
|
+
export declare const makeWorkingMemory: (options?: Options) => Effect.Effect<Memory.Interface>;
|
|
18
|
+
/** @experimental */
|
|
19
|
+
export declare const layer: (options?: Options) => Layer.Layer<Memory.Memory>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Effect, HashMap, Layer, Ref } from "effect";
|
|
2
|
+
import { LanguageModel, Prompt, Toolkit } from "effect/unstable/ai";
|
|
3
|
+
import { Memory } from "@batonfx/core";
|
|
4
|
+
const emptyState = {
|
|
5
|
+
recent: [],
|
|
6
|
+
counter: 0,
|
|
7
|
+
};
|
|
8
|
+
const defaultSummaryPrompt = "Summarize the conversation memory while preserving stable user preferences and facts.";
|
|
9
|
+
const errorMessage = (error) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error));
|
|
10
|
+
const memoryError = (error) => new Memory.MemoryError({ message: errorMessage(error) });
|
|
11
|
+
const keyId = (key) => JSON.stringify([key.agent, key.subject]);
|
|
12
|
+
const textPart = (text) => Prompt.makePart("text", { text });
|
|
13
|
+
const textFromParts = (parts) => parts
|
|
14
|
+
.filter((part) => part.type === "text")
|
|
15
|
+
.map((part) => part.text)
|
|
16
|
+
.join("");
|
|
17
|
+
const roleLabel = (role) => (role === "user" ? "User" : "Assistant");
|
|
18
|
+
const formatItem = (item) => `${roleLabel(item.role)}: ${item.text}`;
|
|
19
|
+
const normalize = (prompt) => prompt.content.flatMap((message) => {
|
|
20
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
21
|
+
return [];
|
|
22
|
+
const text = textFromParts(message.content).trim();
|
|
23
|
+
return text.length === 0 ? [] : [{ role: message.role, text }];
|
|
24
|
+
});
|
|
25
|
+
const sameItem = (left, right) => left.role === right.role && left.text === right.text;
|
|
26
|
+
const appendStart = (recent, incoming) => {
|
|
27
|
+
if (recent.length === 0)
|
|
28
|
+
return 0;
|
|
29
|
+
for (let start = Math.max(0, incoming.length - recent.length); start >= 0; start -= 1) {
|
|
30
|
+
if (start + recent.length > incoming.length)
|
|
31
|
+
continue;
|
|
32
|
+
let matches = true;
|
|
33
|
+
for (let offset = 0; offset < recent.length; offset += 1) {
|
|
34
|
+
const stored = recent[offset];
|
|
35
|
+
const next = incoming[start + offset];
|
|
36
|
+
if (stored === undefined || next === undefined || !sameItem(stored, next)) {
|
|
37
|
+
matches = false;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (matches)
|
|
42
|
+
return start + recent.length;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
};
|
|
46
|
+
const trimToWindow = (items, maxMessages) => {
|
|
47
|
+
if (items.length <= maxMessages)
|
|
48
|
+
return { overflow: [], recent: items };
|
|
49
|
+
if (maxMessages === 0)
|
|
50
|
+
return { overflow: items, recent: [] };
|
|
51
|
+
return {
|
|
52
|
+
overflow: items.slice(0, items.length - maxMessages),
|
|
53
|
+
recent: items.slice(-maxMessages),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const renderSummaryPrompt = (prompt, summary, overflow) => [
|
|
57
|
+
prompt,
|
|
58
|
+
...(summary === undefined ? [] : [`Existing summary:\n${summary}`]),
|
|
59
|
+
`New messages:\n${overflow.map(formatItem).join("\n")}`,
|
|
60
|
+
"Return only the updated summary.",
|
|
61
|
+
].join("\n\n");
|
|
62
|
+
const summarizeOverflow = (options, summary, overflow) => Effect.gen(function* () {
|
|
63
|
+
const model = yield* LanguageModel.LanguageModel;
|
|
64
|
+
const response = yield* model.generateText({
|
|
65
|
+
prompt: renderSummaryPrompt(options.prompt ?? defaultSummaryPrompt, summary, overflow),
|
|
66
|
+
toolkit: Toolkit.empty,
|
|
67
|
+
toolChoice: "none",
|
|
68
|
+
});
|
|
69
|
+
const text = response.text.trim();
|
|
70
|
+
return text.length === 0 ? summary : text;
|
|
71
|
+
}).pipe(Effect.provide(options.model), Effect.mapError(memoryError));
|
|
72
|
+
const recallItems = (state) => [
|
|
73
|
+
...(state.summary === undefined
|
|
74
|
+
? []
|
|
75
|
+
: [
|
|
76
|
+
{
|
|
77
|
+
id: "working-summary",
|
|
78
|
+
parts: [textPart(`<working-memory-summary>\n${state.summary}\n</working-memory-summary>`)],
|
|
79
|
+
},
|
|
80
|
+
]),
|
|
81
|
+
...state.recent.map((item) => ({ id: item.id, parts: [textPart(formatItem(item))] })),
|
|
82
|
+
];
|
|
83
|
+
/** @experimental */
|
|
84
|
+
export const make = (options = {}) => Ref.make(HashMap.empty()).pipe(Effect.map((states) => {
|
|
85
|
+
const maxMessages = Math.max(0, Math.floor(options.maxMessages ?? 20));
|
|
86
|
+
return {
|
|
87
|
+
recall: (input) => Ref.get(states).pipe(Effect.map((current) => HashMap.get(current, keyId(input.key))), Effect.map((state) => (state._tag === "Some" ? recallItems(state.value) : []))),
|
|
88
|
+
remember: (input) => {
|
|
89
|
+
const incoming = normalize(input.transcript);
|
|
90
|
+
if (incoming.length === 0)
|
|
91
|
+
return Effect.void;
|
|
92
|
+
return Effect.gen(function* () {
|
|
93
|
+
const current = yield* Ref.get(states);
|
|
94
|
+
const id = keyId(input.key);
|
|
95
|
+
const existing = HashMap.get(current, id).pipe((option) => option._tag === "Some" ? option.value : emptyState);
|
|
96
|
+
const start = appendStart(existing.recent, incoming);
|
|
97
|
+
const appended = incoming.slice(start);
|
|
98
|
+
if (appended.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
let counter = existing.counter;
|
|
101
|
+
const stored = appended.map((item) => {
|
|
102
|
+
counter += 1;
|
|
103
|
+
return { id: `working-${counter}`, role: item.role, text: item.text };
|
|
104
|
+
});
|
|
105
|
+
const combined = [...existing.recent, ...stored];
|
|
106
|
+
const trimmed = trimToWindow(combined, maxMessages);
|
|
107
|
+
const summary = trimmed.overflow.length === 0
|
|
108
|
+
? existing.summary
|
|
109
|
+
: options.summarize === undefined
|
|
110
|
+
? existing.summary
|
|
111
|
+
: yield* summarizeOverflow(options.summarize, existing.summary, trimmed.overflow);
|
|
112
|
+
const nextState = {
|
|
113
|
+
recent: trimmed.recent,
|
|
114
|
+
counter,
|
|
115
|
+
...(summary === undefined ? {} : { summary }),
|
|
116
|
+
};
|
|
117
|
+
yield* Ref.update(states, HashMap.set(id, nextState));
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
forget: (input) => Ref.update(states, (current) => {
|
|
121
|
+
const id = keyId(input.key);
|
|
122
|
+
if (input.id === undefined)
|
|
123
|
+
return HashMap.remove(current, id);
|
|
124
|
+
const existing = HashMap.get(current, id);
|
|
125
|
+
if (existing._tag === "None")
|
|
126
|
+
return current;
|
|
127
|
+
const summary = input.id === "working-summary" ? undefined : existing.value.summary;
|
|
128
|
+
const recent = existing.value.recent.filter((item) => item.id !== input.id);
|
|
129
|
+
if (summary === undefined && recent.length === 0)
|
|
130
|
+
return HashMap.remove(current, id);
|
|
131
|
+
const nextState = {
|
|
132
|
+
recent,
|
|
133
|
+
counter: existing.value.counter,
|
|
134
|
+
...(summary === undefined ? {} : { summary }),
|
|
135
|
+
};
|
|
136
|
+
return HashMap.set(current, id, nextState);
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}));
|
|
140
|
+
/** @experimental */
|
|
141
|
+
export const makeWorkingMemory = make;
|
|
142
|
+
/** @experimental */
|
|
143
|
+
export const layer = (options = {}) => Layer.effect(Memory.Memory, make(options).pipe(Effect.map(Memory.Memory.of)));
|
package/package.json
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@batonfx/memory",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
|
8
|
-
".":
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
9
12
|
},
|
|
10
13
|
"scripts": {
|
|
11
|
-
"build": "
|
|
14
|
+
"build": "rm -rf dist && bun tsc -p tsconfig.build.json",
|
|
12
15
|
"lint": "bun --cwd ../.. oxlint packages/memory/src packages/memory/test",
|
|
13
16
|
"test": "bun --cwd ../.. vitest run packages/memory/test --passWithNoTests",
|
|
14
17
|
"typecheck": "bun tsc --noEmit"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@batonfx/core": "0.
|
|
20
|
+
"@batonfx/core": "0.4.1",
|
|
18
21
|
"effect": "4.0.0-beta.93"
|
|
19
22
|
},
|
|
20
23
|
"devDependencies": {
|
|
@@ -22,5 +25,27 @@
|
|
|
22
25
|
"@types/bun": "1.3.13",
|
|
23
26
|
"typescript": "5.8.2",
|
|
24
27
|
"vitest": "4.0.16"
|
|
28
|
+
},
|
|
29
|
+
"description": "Non-durable memory implementations for Baton agents",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22",
|
|
37
|
+
"bun": ">=1.3.0"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/In-Time-Tec/batonfx.git",
|
|
42
|
+
"directory": "packages/memory"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/In-Time-Tec/batonfx#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/In-Time-Tec/batonfx/issues"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
25
50
|
}
|
|
26
51
|
}
|
package/.turbo/turbo-build.log
DELETED
package/src/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { Effect, Layer } from "effect"
|
|
2
|
-
import { EmbeddingModel } from "effect/unstable/ai"
|
|
3
|
-
import { Memory } from "@batonfx/core"
|
|
4
|
-
import { makeSemanticRecall } from "./semantic-recall"
|
|
5
|
-
import { VectorStore } from "./vector-store"
|
|
6
|
-
import { makeWorkingMemory } from "./working-memory"
|
|
7
|
-
|
|
8
|
-
export * as SemanticRecall from "./semantic-recall"
|
|
9
|
-
export * as VectorStore from "./vector-store"
|
|
10
|
-
export * as WorkingMemory from "./working-memory"
|
|
11
|
-
|
|
12
|
-
/** @experimental */
|
|
13
|
-
export interface CombinedOptions {
|
|
14
|
-
readonly working?: import("./working-memory").Options
|
|
15
|
-
readonly semantic?: import("./semantic-recall").Options
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** @experimental */
|
|
19
|
-
export const combinedLayer = (
|
|
20
|
-
options: CombinedOptions = {},
|
|
21
|
-
): Layer.Layer<Memory.Memory, never, VectorStore | EmbeddingModel.EmbeddingModel> =>
|
|
22
|
-
Layer.effect(
|
|
23
|
-
Memory.Memory,
|
|
24
|
-
Effect.gen(function* () {
|
|
25
|
-
const working = yield* makeWorkingMemory(options.working)
|
|
26
|
-
const semantic = yield* makeSemanticRecall(options.semantic)
|
|
27
|
-
return Memory.Memory.of(Memory.merge(working, semantic))
|
|
28
|
-
}),
|
|
29
|
-
)
|
package/src/semantic-recall.ts
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Effect, Layer, Ref } from "effect"
|
|
2
|
-
import { EmbeddingModel, Prompt } from "effect/unstable/ai"
|
|
3
|
-
import { Memory } from "@batonfx/core"
|
|
4
|
-
import { VectorStore } from "./vector-store"
|
|
5
|
-
import type { Match } from "./vector-store"
|
|
6
|
-
/** @experimental */
|
|
7
|
-
export interface Options {
|
|
8
|
-
readonly limit?: number
|
|
9
|
-
readonly minScore?: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const errorMessage = (error: unknown) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error))
|
|
13
|
-
|
|
14
|
-
const memoryError = (error: unknown): Memory.MemoryError => new Memory.MemoryError({ message: errorMessage(error) })
|
|
15
|
-
|
|
16
|
-
const textPart = (text: string) => Prompt.makePart("text", { text })
|
|
17
|
-
|
|
18
|
-
const textFromParts = (parts: ReadonlyArray<Prompt.Part>): string =>
|
|
19
|
-
parts
|
|
20
|
-
.filter((part): part is Prompt.TextPart => part.type === "text")
|
|
21
|
-
.map((part) => part.text)
|
|
22
|
-
.join("")
|
|
23
|
-
|
|
24
|
-
const userText = (prompt: Prompt.Prompt): string =>
|
|
25
|
-
prompt.content
|
|
26
|
-
.filter((message): message is Prompt.UserMessage => message.role === "user")
|
|
27
|
-
.map((message) => textFromParts(message.content))
|
|
28
|
-
.filter((text) => text.length > 0)
|
|
29
|
-
.join("\n\n")
|
|
30
|
-
|
|
31
|
-
const finalExchangeText = (prompt: Prompt.Prompt): string | undefined => {
|
|
32
|
-
let assistant: string | undefined
|
|
33
|
-
let assistantIndex = -1
|
|
34
|
-
for (let index = prompt.content.length - 1; index >= 0; index -= 1) {
|
|
35
|
-
const message = prompt.content[index]
|
|
36
|
-
if (message?.role !== "assistant") continue
|
|
37
|
-
const text = textFromParts(message.content).trim()
|
|
38
|
-
if (text.length === 0) continue
|
|
39
|
-
assistant = text
|
|
40
|
-
assistantIndex = index
|
|
41
|
-
break
|
|
42
|
-
}
|
|
43
|
-
if (assistant === undefined) return undefined
|
|
44
|
-
for (let index = assistantIndex - 1; index >= 0; index -= 1) {
|
|
45
|
-
const message = prompt.content[index]
|
|
46
|
-
if (message?.role !== "user") continue
|
|
47
|
-
const text = textFromParts(message.content).trim()
|
|
48
|
-
if (text.length === 0) continue
|
|
49
|
-
return `User: ${text}\nAssistant: ${assistant}`
|
|
50
|
-
}
|
|
51
|
-
return undefined
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const itemFromMatch = (match: Match): Memory.Item => ({
|
|
55
|
-
id: match.document.id,
|
|
56
|
-
parts: [textPart(match.document.text)],
|
|
57
|
-
metadata: { ...match.document.metadata, score: match.score },
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
/** @experimental */
|
|
61
|
-
export const make = (
|
|
62
|
-
options: Options = {},
|
|
63
|
-
): Effect.Effect<Memory.Interface, never, VectorStore | EmbeddingModel.EmbeddingModel> =>
|
|
64
|
-
Effect.gen(function* () {
|
|
65
|
-
const store = yield* VectorStore
|
|
66
|
-
const embeddingModel = yield* EmbeddingModel.EmbeddingModel
|
|
67
|
-
const counter = yield* Ref.make(0)
|
|
68
|
-
const limit = options.limit ?? 5
|
|
69
|
-
|
|
70
|
-
return {
|
|
71
|
-
recall: (input) => {
|
|
72
|
-
const text = userText(input.prompt)
|
|
73
|
-
if (text.length === 0) return Effect.succeed([])
|
|
74
|
-
return embeddingModel.embed(text).pipe(
|
|
75
|
-
Effect.mapError(memoryError),
|
|
76
|
-
Effect.flatMap((embedding) =>
|
|
77
|
-
store
|
|
78
|
-
.query({
|
|
79
|
-
key: input.key,
|
|
80
|
-
embedding: embedding.vector,
|
|
81
|
-
limit,
|
|
82
|
-
...(options.minScore === undefined ? {} : { minScore: options.minScore }),
|
|
83
|
-
})
|
|
84
|
-
.pipe(Effect.mapError(memoryError)),
|
|
85
|
-
),
|
|
86
|
-
Effect.map((matches) => matches.map(itemFromMatch)),
|
|
87
|
-
)
|
|
88
|
-
},
|
|
89
|
-
remember: (input) => {
|
|
90
|
-
if (!input.terminal) return Effect.void
|
|
91
|
-
const text = finalExchangeText(input.transcript)
|
|
92
|
-
if (text === undefined) return Effect.void
|
|
93
|
-
return embeddingModel.embed(text).pipe(
|
|
94
|
-
Effect.mapError(memoryError),
|
|
95
|
-
Effect.flatMap((embedding) =>
|
|
96
|
-
Ref.modify(counter, (current) => [`semantic-${current + 1}`, current + 1]).pipe(
|
|
97
|
-
Effect.flatMap((id) =>
|
|
98
|
-
store.upsert([
|
|
99
|
-
{
|
|
100
|
-
id,
|
|
101
|
-
key: input.key,
|
|
102
|
-
text,
|
|
103
|
-
embedding: embedding.vector,
|
|
104
|
-
},
|
|
105
|
-
]),
|
|
106
|
-
),
|
|
107
|
-
),
|
|
108
|
-
),
|
|
109
|
-
Effect.mapError(memoryError),
|
|
110
|
-
)
|
|
111
|
-
},
|
|
112
|
-
forget: (input) => store.delete(input).pipe(Effect.mapError(memoryError)),
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
/** @experimental */
|
|
117
|
-
export const makeSemanticRecall = make
|
|
118
|
-
|
|
119
|
-
/** @experimental */
|
|
120
|
-
export const layer = (
|
|
121
|
-
options: Options = {},
|
|
122
|
-
): Layer.Layer<Memory.Memory, never, VectorStore | EmbeddingModel.EmbeddingModel> =>
|
|
123
|
-
Layer.effect(Memory.Memory, make(options).pipe(Effect.map(Memory.Memory.of)))
|