@brainbank/memory 0.1.0

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 ADDED
@@ -0,0 +1,189 @@
1
+ # @brainbank/memory
2
+
3
+ Deterministic memory extraction and deduplication for LLM conversations. Inspired by [mem0](https://github.com/mem0ai/mem0)'s pipeline.
4
+
5
+ After every conversation turn, automatically:
6
+
7
+ 1. **Extract** atomic facts via LLM call
8
+ 2. **Search** existing memories for duplicates
9
+ 3. **Decide** ADD / UPDATE / NONE per fact
10
+ 4. **Execute** the operations
11
+
12
+ No function calling. No relying on the model to "remember" to save.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @brainbank/memory
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { BrainBank } from 'brainbank';
24
+ import { Memory, OpenAIProvider } from '@brainbank/memory';
25
+
26
+ const brain = new BrainBank({ dbPath: './memory.db' });
27
+ await brain.initialize();
28
+
29
+ const memory = new Memory(brain.collection('memories'), {
30
+ llm: new OpenAIProvider({ model: 'gpt-4.1-nano' }),
31
+ });
32
+
33
+ // After every conversation turn — deterministic, automatic
34
+ const ops = await memory.process(
35
+ 'My name is Berna, I prefer TypeScript',
36
+ 'Nice to meet you Berna!'
37
+ );
38
+ // ops → [
39
+ // { fact: "User's name is Berna", action: "ADD", reason: "no similar memories" },
40
+ // { fact: "User prefers TypeScript", action: "ADD", reason: "no similar memories" }
41
+ // ]
42
+
43
+ // Next turn — dedup kicks in
44
+ await memory.process(
45
+ 'I like TypeScript a lot',
46
+ 'TypeScript is great!'
47
+ );
48
+ // → [{ fact: "User likes TypeScript", action: "NONE", reason: "already captured" }]
49
+
50
+ // Build system prompt context
51
+ const context = memory.buildContext();
52
+ // → "## Memories\n- User's name is Berna\n- User prefers TypeScript"
53
+
54
+ // Semantic search
55
+ const results = await memory.search('what language does user prefer');
56
+ ```
57
+
58
+ ## Framework Integration
59
+
60
+ The `LLMProvider` interface is framework-agnostic. Bring your own LLM:
61
+
62
+ ### LangChain
63
+
64
+ ```typescript
65
+ import { ChatOpenAI } from '@langchain/openai';
66
+ import { Memory } from '@brainbank/memory';
67
+ import type { LLMProvider } from '@brainbank/memory';
68
+
69
+ const model = new ChatOpenAI({ model: 'gpt-4.1-nano' });
70
+
71
+ const llm: LLMProvider = {
72
+ generate: async (messages, opts) => {
73
+ const res = await model.invoke(messages);
74
+ return res.content as string;
75
+ }
76
+ };
77
+
78
+ const memory = new Memory(store, { llm });
79
+ ```
80
+
81
+ ### Vercel AI SDK
82
+
83
+ ```typescript
84
+ import { generateText } from 'ai';
85
+ import { openai } from '@ai-sdk/openai';
86
+ import { Memory } from '@brainbank/memory';
87
+ import type { LLMProvider } from '@brainbank/memory';
88
+
89
+ const llm: LLMProvider = {
90
+ generate: async (messages) => {
91
+ const { text } = await generateText({
92
+ model: openai('gpt-4.1-nano'),
93
+ messages,
94
+ });
95
+ return text;
96
+ }
97
+ };
98
+
99
+ const memory = new Memory(store, { llm });
100
+ ```
101
+
102
+ ### Anthropic / Other Providers
103
+
104
+ ```typescript
105
+ const llm: LLMProvider = {
106
+ generate: async (messages) => {
107
+ // Call any LLM API that takes messages and returns a string
108
+ const response = await yourLLMClient.chat(messages);
109
+ return response.text;
110
+ }
111
+ };
112
+ ```
113
+
114
+ ## Custom Storage
115
+
116
+ The `MemoryStore` interface matches BrainBank collections, but you can implement your own:
117
+
118
+ ```typescript
119
+ import type { MemoryStore } from '@brainbank/memory';
120
+
121
+ const store: MemoryStore = {
122
+ add: async (content, opts) => { /* store in your DB */ },
123
+ search: async (query, opts) => { /* semantic search */ },
124
+ list: (opts) => { /* return recent items */ },
125
+ remove: async (id) => { /* delete by ID */ },
126
+ count: () => { /* return total */ },
127
+ };
128
+
129
+ const memory = new Memory(store, { llm });
130
+ ```
131
+
132
+ ## Options
133
+
134
+ ```typescript
135
+ new Memory(store, {
136
+ llm: provider, // required — LLM provider
137
+ maxFacts: 5, // max facts to extract per turn (default: 5)
138
+ maxMemories: 50, // max existing memories to load for dedup (default: 50)
139
+ dedupTopK: 3, // similar memories to compare against (default: 3)
140
+ extractPrompt: '...', // custom extraction prompt
141
+ dedupPrompt: '...', // custom dedup prompt
142
+ onOperation: (op) => { // callback for each operation
143
+ console.log(`${op.action}: ${op.fact}`);
144
+ },
145
+ });
146
+ ```
147
+
148
+ ## API
149
+
150
+ | Method | Description |
151
+ |--------|-------------|
152
+ | `process(userMsg, assistantMsg)` | Run the full pipeline: extract → dedup → execute. Returns `MemoryOperation[]` |
153
+ | `search(query, k?)` | Semantic search across memories |
154
+ | `recall(limit?)` | Get all memories (for system prompt injection) |
155
+ | `count()` | Total stored memories |
156
+ | `buildContext(limit?)` | Build a markdown section for system prompt injection |
157
+
158
+ ## How it works
159
+
160
+ ```
161
+ User message + Assistant response
162
+
163
+
164
+ ┌─── Extract (LLM) ───┐
165
+ │ "User's name is X" │
166
+ │ "Prefers TypeScript" │
167
+ └──────────┬───────────┘
168
+ │ for each fact:
169
+
170
+ ┌─── Search (semantic) ─┐
171
+ │ Find similar existing │
172
+ │ memories (top-K) │
173
+ └──────────┬────────────┘
174
+
175
+
176
+ ┌─── Dedup (LLM) ──────┐
177
+ │ Compare new vs existing│
178
+ │ → ADD / UPDATE / NONE │
179
+ └──────────┬────────────┘
180
+
181
+ ┌───────┼───────┐
182
+ ▼ ▼ ▼
183
+ ADD UPDATE NONE
184
+ (store) (replace) (skip)
185
+ ```
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,157 @@
1
+ /**
2
+ * @brainbank/memory — LLM Provider Interface
3
+ *
4
+ * Framework-agnostic interface for LLM calls.
5
+ * Implement this to use any LLM: OpenAI, Anthropic, LangChain, Vercel AI SDK, etc.
6
+ */
7
+ interface ChatMessage {
8
+ role: 'system' | 'user' | 'assistant';
9
+ content: string;
10
+ }
11
+ interface GenerateOptions {
12
+ /** Request JSON output */
13
+ json?: boolean;
14
+ /** Max tokens for response */
15
+ maxTokens?: number;
16
+ }
17
+ /**
18
+ * LLM provider interface. Implement this to bring your own model.
19
+ *
20
+ * @example OpenAI
21
+ * ```typescript
22
+ * const llm = new OpenAIProvider({ apiKey: 'sk-...', model: 'gpt-4.1-nano' });
23
+ * ```
24
+ *
25
+ * @example LangChain
26
+ * ```typescript
27
+ * import { ChatOpenAI } from '@langchain/openai';
28
+ * const model = new ChatOpenAI({ model: 'gpt-4.1-nano' });
29
+ * const llm: LLMProvider = {
30
+ * generate: async (messages, opts) => {
31
+ * const res = await model.invoke(messages);
32
+ * return res.content as string;
33
+ * }
34
+ * };
35
+ * ```
36
+ *
37
+ * @example Vercel AI SDK
38
+ * ```typescript
39
+ * import { generateText } from 'ai';
40
+ * import { openai } from '@ai-sdk/openai';
41
+ * const llm: LLMProvider = {
42
+ * generate: async (messages) => {
43
+ * const { text } = await generateText({ model: openai('gpt-4.1-nano'), messages });
44
+ * return text;
45
+ * }
46
+ * };
47
+ * ```
48
+ */
49
+ interface LLMProvider {
50
+ generate(messages: ChatMessage[], options?: GenerateOptions): Promise<string>;
51
+ }
52
+ interface OpenAIProviderOptions {
53
+ /** OpenAI API key. Defaults to OPENAI_API_KEY env var. */
54
+ apiKey?: string;
55
+ /** Model name. Default: gpt-4.1-nano */
56
+ model?: string;
57
+ /** Base URL for API. Default: https://api.openai.com/v1 */
58
+ baseUrl?: string;
59
+ }
60
+ declare class OpenAIProvider implements LLMProvider {
61
+ private readonly apiKey;
62
+ private readonly model;
63
+ private readonly baseUrl;
64
+ constructor(options?: OpenAIProviderOptions);
65
+ generate(messages: ChatMessage[], options?: GenerateOptions): Promise<string>;
66
+ }
67
+
68
+ /**
69
+ * @brainbank/memory — Deterministic Memory Pipeline
70
+ *
71
+ * Automatic fact extraction and deduplication for LLM conversations.
72
+ * Runs after every turn: extract → search → dedup → ADD/UPDATE/NONE.
73
+ */
74
+
75
+ interface MemoryItem {
76
+ id?: string | number;
77
+ content: string;
78
+ score?: number;
79
+ metadata?: Record<string, any>;
80
+ }
81
+ type MemoryAction = 'ADD' | 'UPDATE' | 'NONE';
82
+ interface MemoryOperation {
83
+ fact: string;
84
+ action: MemoryAction;
85
+ reason: string;
86
+ }
87
+ /**
88
+ * Collection interface — matches BrainBank's collection API.
89
+ * Implement this to use a different storage backend.
90
+ */
91
+ interface MemoryStore {
92
+ add(content: string, options?: {
93
+ tags?: string[];
94
+ metadata?: Record<string, any>;
95
+ }): Promise<any>;
96
+ search(query: string, options?: {
97
+ k?: number;
98
+ }): Promise<MemoryItem[]>;
99
+ list(options?: {
100
+ limit?: number;
101
+ }): MemoryItem[];
102
+ remove(id: string | number): void | Promise<void>;
103
+ count(): number;
104
+ }
105
+ interface MemoryOptions {
106
+ /** LLM provider for extraction and dedup */
107
+ llm: LLMProvider;
108
+ /** Max facts to extract per turn. Default: 5 */
109
+ maxFacts?: number;
110
+ /** Max existing memories to compare against for dedup. Default: 50 */
111
+ maxMemories?: number;
112
+ /** Number of similar memories to check for dedup. Default: 3 */
113
+ dedupTopK?: number;
114
+ /** Custom extraction prompt (replaces default) */
115
+ extractPrompt?: string;
116
+ /** Custom dedup prompt (replaces default) */
117
+ dedupPrompt?: string;
118
+ /** Called for each memory operation */
119
+ onOperation?: (op: MemoryOperation) => void;
120
+ }
121
+ declare class Memory {
122
+ private readonly store;
123
+ private readonly llm;
124
+ private readonly maxFacts;
125
+ private readonly maxMemories;
126
+ private readonly dedupTopK;
127
+ private readonly extractPrompt;
128
+ private readonly dedupPrompt;
129
+ private readonly onOperation?;
130
+ constructor(store: MemoryStore, options: MemoryOptions);
131
+ /**
132
+ * Process a conversation turn — extract facts and store/update memories.
133
+ * This is the main entry point. Call after every user↔assistant exchange.
134
+ */
135
+ process(userMessage: string, assistantMessage: string): Promise<MemoryOperation[]>;
136
+ /**
137
+ * Search memories semantically.
138
+ */
139
+ search(query: string, k?: number): Promise<MemoryItem[]>;
140
+ /**
141
+ * Get all memories (for system prompt injection).
142
+ */
143
+ recall(limit?: number): MemoryItem[];
144
+ /**
145
+ * Get memory count.
146
+ */
147
+ count(): number;
148
+ /**
149
+ * Build a system prompt section with all memories.
150
+ * Drop this into your system prompt.
151
+ */
152
+ buildContext(limit?: number): string;
153
+ private extract;
154
+ private dedup;
155
+ }
156
+
157
+ export { type ChatMessage, type GenerateOptions, type LLMProvider, Memory, type MemoryAction, type MemoryItem, type MemoryOperation, type MemoryOptions, type MemoryStore, OpenAIProvider, type OpenAIProviderOptions };
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ // src/prompts.ts
2
+ var EXTRACT_PROMPT = `You are a memory extraction engine. Given a conversation turn between a user and an assistant, extract distinct atomic facts worth remembering for future conversations.
3
+
4
+ Focus on:
5
+ - User preferences (language, tools, patterns, style)
6
+ - User personal info (name, role, projects)
7
+ - Decisions made (architecture, design, technology choices)
8
+ - Important context (deadlines, constraints, goals)
9
+
10
+ Respond with JSON: { "facts": ["fact1", "fact2", ...] }
11
+ If nothing is worth remembering, return: { "facts": [] }
12
+
13
+ Rules:
14
+ - Each fact must be a single, self-contained sentence
15
+ - Be specific ("prefers TypeScript" not "has programming preferences")
16
+ - Skip trivial info ("said hello", "asked a question")
17
+ - Max 5 facts per turn`;
18
+ var DEDUP_PROMPT = `You are a memory deduplication engine. Given a NEW fact and a list of EXISTING memories, decide what action to take.
19
+
20
+ Respond with JSON: { "action": "ADD" | "UPDATE" | "NONE", "reason": "brief reason" }
21
+
22
+ - ADD: the fact is genuinely new information not covered by any existing memory
23
+ - UPDATE: the fact updates, corrects, or expands an existing memory (include "update_index" field: 0-based index)
24
+ - NONE: the fact is already well-captured by existing memories
25
+
26
+ Be conservative \u2014 if in doubt, say NONE.`;
27
+
28
+ // src/memory.ts
29
+ var Memory = class {
30
+ store;
31
+ llm;
32
+ maxFacts;
33
+ maxMemories;
34
+ dedupTopK;
35
+ extractPrompt;
36
+ dedupPrompt;
37
+ onOperation;
38
+ constructor(store, options) {
39
+ this.store = store;
40
+ this.llm = options.llm;
41
+ this.maxFacts = options.maxFacts ?? 5;
42
+ this.maxMemories = options.maxMemories ?? 50;
43
+ this.dedupTopK = options.dedupTopK ?? 3;
44
+ this.extractPrompt = options.extractPrompt ?? EXTRACT_PROMPT;
45
+ this.dedupPrompt = options.dedupPrompt ?? DEDUP_PROMPT;
46
+ this.onOperation = options.onOperation;
47
+ }
48
+ /**
49
+ * Process a conversation turn — extract facts and store/update memories.
50
+ * This is the main entry point. Call after every user↔assistant exchange.
51
+ */
52
+ async process(userMessage, assistantMessage) {
53
+ const facts = await this.extract(userMessage, assistantMessage);
54
+ if (facts.length === 0) return [];
55
+ const existing = this.store.list({ limit: this.maxMemories });
56
+ const operations = [];
57
+ for (const fact of facts) {
58
+ const op = await this.dedup(fact, existing);
59
+ operations.push(op);
60
+ this.onOperation?.(op);
61
+ switch (op.action) {
62
+ case "ADD":
63
+ await this.store.add(fact);
64
+ break;
65
+ case "UPDATE": {
66
+ const similar = await this.store.search(fact, { k: this.dedupTopK });
67
+ const target = similar[0];
68
+ if (target?.id) {
69
+ await this.store.remove(target.id);
70
+ await this.store.add(fact);
71
+ }
72
+ break;
73
+ }
74
+ case "NONE":
75
+ break;
76
+ }
77
+ }
78
+ return operations;
79
+ }
80
+ /**
81
+ * Search memories semantically.
82
+ */
83
+ async search(query, k = 5) {
84
+ return this.store.search(query, { k });
85
+ }
86
+ /**
87
+ * Get all memories (for system prompt injection).
88
+ */
89
+ recall(limit = 20) {
90
+ return this.store.list({ limit });
91
+ }
92
+ /**
93
+ * Get memory count.
94
+ */
95
+ count() {
96
+ return this.store.count();
97
+ }
98
+ /**
99
+ * Build a system prompt section with all memories.
100
+ * Drop this into your system prompt.
101
+ */
102
+ buildContext(limit = 20) {
103
+ const items = this.store.list({ limit });
104
+ if (items.length === 0) return "";
105
+ return "## Memories\n" + items.map((m) => `- ${m.content}`).join("\n");
106
+ }
107
+ // ─── Internal ───────────────────────────────────
108
+ async extract(userMsg, assistantMsg) {
109
+ const response = await this.llm.generate([
110
+ { role: "system", content: this.extractPrompt },
111
+ { role: "user", content: `User: ${userMsg}
112
+
113
+ Assistant: ${assistantMsg}` }
114
+ ], { json: true, maxTokens: 300 });
115
+ try {
116
+ const parsed = JSON.parse(response);
117
+ const facts = parsed.facts ?? [];
118
+ return facts.slice(0, this.maxFacts);
119
+ } catch {
120
+ return [];
121
+ }
122
+ }
123
+ async dedup(fact, _existing) {
124
+ const similar = await this.store.search(fact, { k: this.dedupTopK });
125
+ if (similar.length === 0) {
126
+ return { fact, action: "ADD", reason: "no similar memories found" };
127
+ }
128
+ const context = similar.map((m, i) => `[${i}] ${m.content}`).join("\n");
129
+ const response = await this.llm.generate([
130
+ { role: "system", content: this.dedupPrompt },
131
+ { role: "user", content: `NEW FACT: "${fact}"
132
+
133
+ EXISTING MEMORIES:
134
+ ${context}` }
135
+ ], { json: true, maxTokens: 150 });
136
+ try {
137
+ const parsed = JSON.parse(response);
138
+ return {
139
+ fact,
140
+ action: parsed.action ?? "ADD",
141
+ reason: parsed.reason ?? ""
142
+ };
143
+ } catch {
144
+ return { fact, action: "ADD", reason: "parse error, defaulting to ADD" };
145
+ }
146
+ }
147
+ };
148
+
149
+ // src/llm.ts
150
+ var OpenAIProvider = class {
151
+ apiKey;
152
+ model;
153
+ baseUrl;
154
+ constructor(options = {}) {
155
+ this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
156
+ this.model = options.model ?? "gpt-4.1-nano";
157
+ this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1";
158
+ if (!this.apiKey) {
159
+ throw new Error("@brainbank/memory: OPENAI_API_KEY is required for OpenAIProvider");
160
+ }
161
+ }
162
+ async generate(messages, options) {
163
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
164
+ method: "POST",
165
+ headers: {
166
+ "Content-Type": "application/json",
167
+ Authorization: `Bearer ${this.apiKey}`
168
+ },
169
+ body: JSON.stringify({
170
+ model: this.model,
171
+ messages,
172
+ max_tokens: options?.maxTokens ?? 500,
173
+ ...options?.json ? { response_format: { type: "json_object" } } : {}
174
+ })
175
+ });
176
+ if (!res.ok) {
177
+ throw new Error(`OpenAI error ${res.status}: ${await res.text()}`);
178
+ }
179
+ const data = await res.json();
180
+ return data.choices?.[0]?.message?.content ?? "";
181
+ }
182
+ };
183
+ export {
184
+ Memory,
185
+ OpenAIProvider
186
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@brainbank/memory",
3
+ "version": "0.1.0",
4
+ "description": "Deterministic memory extraction and deduplication for LLM conversations — extract, dedup, ADD/UPDATE/NONE",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" }
10
+ },
11
+ "files": ["dist/"],
12
+ "scripts": {
13
+ "build": "tsup"
14
+ },
15
+ "peerDependencies": {
16
+ "brainbank": ">=0.2.0"
17
+ },
18
+ "peerDependenciesMeta": {
19
+ "brainbank": { "optional": true }
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/pinecall/brainbank.git",
24
+ "directory": "packages/memory"
25
+ },
26
+ "keywords": [
27
+ "memory", "llm", "ai", "agent", "langchain", "deduplication",
28
+ "fact-extraction", "conversation-memory", "rag", "brainbank"
29
+ ],
30
+ "author": "Bernardo Castro <bernardo@pinecall.io>",
31
+ "license": "MIT"
32
+ }