@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 +189 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.js +186 -0
- package/package.json +32 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|