@jeremiaheth/neolata-mem 0.2.0 → 0.3.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 +87 -0
- package/package.json +7 -5
- package/src/embeddings.mjs +120 -21
- package/src/extraction.mjs +147 -35
- package/src/graph.mjs +445 -97
- package/src/llm.mjs +103 -9
- package/src/storage.mjs +150 -9
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**Graph-native memory engine for AI agents.** Zettelkasten-inspired linking, biological decay, conflict resolution.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/@jeremiaheth/neolata-mem)
|
|
6
|
+
[](https://bundlephobia.com/package/@jeremiaheth/neolata-mem)
|
|
5
7
|
[](LICENSE)
|
|
6
8
|
[](https://nodejs.org)
|
|
7
9
|
|
|
@@ -13,6 +15,58 @@ No Python. No Docker. No Neo4j. Just `npm install`.
|
|
|
13
15
|
npm install @jeremiaheth/neolata-mem
|
|
14
16
|
```
|
|
15
17
|
|
|
18
|
+
## Why neolata-mem?
|
|
19
|
+
|
|
20
|
+
**Because AI agents need memory that works like actual memory** — connected, decaying, evolving. Not a flat vector database with timestamps. A living knowledge graph that links related facts, forgets what doesn't matter, and resolves contradictions automatically.
|
|
21
|
+
|
|
22
|
+
## Architecture
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
26
|
+
│ STORE WORKFLOW (A-MEM) │
|
|
27
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
28
|
+
│ │
|
|
29
|
+
│ Text Input │
|
|
30
|
+
│ │ │
|
|
31
|
+
│ ├──► [Embed] ──────────────┐ │
|
|
32
|
+
│ │ ▼ │
|
|
33
|
+
│ │ Find Related Memories │
|
|
34
|
+
│ │ (similarity > threshold) │
|
|
35
|
+
│ │ │ │
|
|
36
|
+
│ │ ▼ │
|
|
37
|
+
│ │ Auto-Link Bidirectionally │
|
|
38
|
+
│ │ (top N similar) │
|
|
39
|
+
│ │ │ │
|
|
40
|
+
│ └────────────────────────── ┘ │
|
|
41
|
+
│ │ │
|
|
42
|
+
│ ▼ │
|
|
43
|
+
│ [Store in Graph] │
|
|
44
|
+
│ Memory + Links + Embedding │
|
|
45
|
+
│ │
|
|
46
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
47
|
+
|
|
48
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
49
|
+
│ DECAY CYCLE │
|
|
50
|
+
├─────────────────────────────────────────────────────────────────────┤
|
|
51
|
+
│ │
|
|
52
|
+
│ For each Memory: │
|
|
53
|
+
│ │ │
|
|
54
|
+
│ ├──► Calculate Strength: │
|
|
55
|
+
│ │ • Base: importance (0.0-1.0) │
|
|
56
|
+
│ │ • Age decay: exp(-age / half-life) │
|
|
57
|
+
│ │ • Link reinforcement: +5% per link (max +30%) │
|
|
58
|
+
│ │ • Category stickiness: decisions 1.3x, preferences 1.4x │
|
|
59
|
+
│ │ • Access boost: +2% per touch (max +20%) │
|
|
60
|
+
│ │ │
|
|
61
|
+
│ ├──► strength < 0.05 ? ──► DELETE │
|
|
62
|
+
│ │ │
|
|
63
|
+
│ ├──► strength < 0.15 ? ──► ARCHIVE (remove embedding, keep) │
|
|
64
|
+
│ │ │
|
|
65
|
+
│ └──► Clean broken links │
|
|
66
|
+
│ │
|
|
67
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
68
|
+
```
|
|
69
|
+
|
|
16
70
|
## Quick Start (3 lines)
|
|
17
71
|
|
|
18
72
|
```javascript
|
|
@@ -292,6 +346,39 @@ Decay Cycle:
|
|
|
292
346
|
| Zero-config start | ✅ | ❌ | ❌ | ❌ |
|
|
293
347
|
| LLM optional | ✅ | ❌ | ❌ | ❌ |
|
|
294
348
|
|
|
349
|
+
## Documentation
|
|
350
|
+
|
|
351
|
+
Full documentation available at [neolata-mem docs](https://github.com/Jeremiaheth/neolata-mem/tree/main/docs-site).
|
|
352
|
+
|
|
353
|
+
Run locally:
|
|
354
|
+
```bash
|
|
355
|
+
cd docs-site
|
|
356
|
+
npm install
|
|
357
|
+
npm start
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Contributing
|
|
361
|
+
|
|
362
|
+
Contributions welcome! This is built by people who break things for a living.
|
|
363
|
+
|
|
364
|
+
**Before submitting:**
|
|
365
|
+
- Run tests: `npm test`
|
|
366
|
+
- Keep the hacker aesthetic — no corporate fluff
|
|
367
|
+
- Code examples must actually work
|
|
368
|
+
- Break it before you ship it
|
|
369
|
+
|
|
370
|
+
**Areas that need work:**
|
|
371
|
+
- More storage backends (Supabase, Redis, SQLite)
|
|
372
|
+
- Additional embedding providers
|
|
373
|
+
- Performance optimizations for 100k+ memory graphs
|
|
374
|
+
- Visualization tools
|
|
375
|
+
|
|
376
|
+
Open an issue or PR. No bureaucracy.
|
|
377
|
+
|
|
378
|
+
## Changelog
|
|
379
|
+
|
|
380
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and breaking changes.
|
|
381
|
+
|
|
295
382
|
## License
|
|
296
383
|
|
|
297
384
|
MIT — do whatever you want.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jeremiaheth/neolata-mem",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Graph-native memory engine for AI agents with Zettelkasten linking, biological decay, and conflict resolution",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.mjs",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"test": "node --test test/*.test.mjs"
|
|
25
|
+
"test": "node --test test/*.test.mjs",
|
|
26
|
+
"lint": "echo 'no linter configured'"
|
|
26
27
|
},
|
|
27
28
|
"keywords": [
|
|
28
29
|
"ai",
|
|
@@ -35,8 +36,7 @@
|
|
|
35
36
|
"llm",
|
|
36
37
|
"vector-search",
|
|
37
38
|
"multi-agent",
|
|
38
|
-
"decay"
|
|
39
|
-
"conflict-resolution"
|
|
39
|
+
"decay"
|
|
40
40
|
],
|
|
41
41
|
"author": "Jeremiaheth",
|
|
42
42
|
"license": "MIT",
|
|
@@ -46,5 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=18.0.0"
|
|
49
|
-
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {},
|
|
51
|
+
"devDependencies": {}
|
|
50
52
|
}
|
package/src/embeddings.mjs
CHANGED
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* @module embeddings
|
|
3
|
+
* @description Embedding provider interface and implementations.
|
|
4
|
+
*
|
|
5
|
+
* All embedding providers must implement the following interface:
|
|
6
|
+
* ```typescript
|
|
7
|
+
* interface EmbeddingProvider {
|
|
8
|
+
* name: string;
|
|
9
|
+
* model: string | null;
|
|
10
|
+
* embed(texts: string | string[]): Promise<(number[] | null)[]>;
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // OpenAI embeddings
|
|
16
|
+
* import { openaiEmbeddings } from '@jeremiaheth/neolata-mem/embeddings';
|
|
17
|
+
* const embedder = openaiEmbeddings({
|
|
18
|
+
* apiKey: process.env.OPENAI_API_KEY,
|
|
19
|
+
* model: 'text-embedding-3-small',
|
|
20
|
+
* });
|
|
21
|
+
* const vectors = await embedder.embed(['Hello world', 'Goodbye world']);
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* // No-op embeddings (keyword search only)
|
|
25
|
+
* import { noopEmbeddings } from '@jeremiaheth/neolata-mem/embeddings';
|
|
26
|
+
* const embedder = noopEmbeddings();
|
|
27
|
+
* const vectors = await embedder.embed(['text']); // → [null]
|
|
4
28
|
*/
|
|
5
29
|
|
|
6
30
|
/**
|
|
7
|
-
*
|
|
8
|
-
* @
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
31
|
+
* @typedef {Object} EmbeddingProvider
|
|
32
|
+
* @property {string} name - Provider identifier
|
|
33
|
+
* @property {string|null} model - Model name or null
|
|
34
|
+
* @property {function(string|string[]): Promise<(number[]|null)[]>} embed - Embed text(s) to vectors
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Calculate cosine similarity between two embedding vectors.
|
|
39
|
+
*
|
|
40
|
+
* @param {number[]} a - First vector
|
|
41
|
+
* @param {number[]} b - Second vector (must be same length as a)
|
|
42
|
+
* @returns {number} Similarity score (0.0 to 1.0, higher = more similar)
|
|
43
|
+
* @throws {Error} If vectors are different lengths or empty
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const sim = cosineSimilarity([1, 0, 0], [1, 0, 0]); // → 1.0 (identical)
|
|
47
|
+
* const sim2 = cosineSimilarity([1, 0, 0], [0, 1, 0]); // → 0.0 (orthogonal)
|
|
11
48
|
*/
|
|
12
49
|
export function cosineSimilarity(a, b) {
|
|
13
50
|
let dot = 0, normA = 0, normB = 0;
|
|
@@ -19,21 +56,66 @@ export function cosineSimilarity(a, b) {
|
|
|
19
56
|
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
20
57
|
}
|
|
21
58
|
|
|
22
|
-
// ─── OpenAI-Compatible Provider ─────────────────────────────
|
|
23
59
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
60
|
+
* OpenAI-compatible embedding provider.
|
|
61
|
+
*
|
|
62
|
+
* Works with any API that implements the OpenAI `/v1/embeddings` endpoint:
|
|
63
|
+
* - OpenAI (text-embedding-3-small, text-embedding-3-large)
|
|
64
|
+
* - NVIDIA NIM (baai/bge-m3, nvidia/nv-embed-v1)
|
|
65
|
+
* - Ollama (nomic-embed-text, mxbai-embed-large)
|
|
66
|
+
* - Azure OpenAI
|
|
67
|
+
* - Groq, Together, etc.
|
|
68
|
+
*
|
|
69
|
+
* **Rate limiting:** Automatically retries on 429 with exponential backoff.
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} opts - Configuration options
|
|
72
|
+
* @param {string} opts.apiKey - API key for authentication
|
|
73
|
+
* @param {string} opts.model - Model identifier (e.g. 'text-embedding-3-small', 'baai/bge-m3')
|
|
74
|
+
* @param {string} [opts.baseUrl='https://api.openai.com/v1'] - API base URL (without /embeddings)
|
|
75
|
+
* @param {Object} [opts.extraBody={}] - Additional request body parameters (e.g. { input_type: 'passage' } for NIM)
|
|
76
|
+
* @param {number} [opts.retryMs=2000] - Base retry delay on 429 in milliseconds
|
|
77
|
+
* @param {number} [opts.maxRetries=3] - Maximum retry attempts on 429
|
|
78
|
+
* @returns {EmbeddingProvider} Configured embedding provider
|
|
79
|
+
* @throws {Error} If apiKey or model is missing
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* // OpenAI
|
|
83
|
+
* const emb = openaiEmbeddings({
|
|
84
|
+
* apiKey: process.env.OPENAI_API_KEY,
|
|
85
|
+
* model: 'text-embedding-3-small',
|
|
86
|
+
* });
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* // NVIDIA NIM (free tier)
|
|
90
|
+
* const emb = openaiEmbeddings({
|
|
91
|
+
* apiKey: process.env.NVIDIA_API_KEY,
|
|
92
|
+
* model: 'baai/bge-m3',
|
|
93
|
+
* baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
94
|
+
* extraBody: { input_type: 'passage', truncate: 'END' },
|
|
95
|
+
* });
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Local Ollama
|
|
99
|
+
* const emb = openaiEmbeddings({
|
|
100
|
+
* apiKey: 'ollama', // Ollama ignores auth
|
|
101
|
+
* model: 'nomic-embed-text',
|
|
102
|
+
* baseUrl: 'http://localhost:11434/v1',
|
|
103
|
+
* });
|
|
31
104
|
*/
|
|
32
|
-
export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.com/v1', extraBody = {}, retryMs = 2000 }) {
|
|
105
|
+
export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.com/v1', extraBody = {}, retryMs = 2000, maxRetries = 3 }) {
|
|
106
|
+
if (!apiKey) throw new Error('openaiEmbeddings: apiKey is required');
|
|
107
|
+
if (!model) throw new Error('openaiEmbeddings: model is required');
|
|
33
108
|
return {
|
|
34
109
|
name: `openai-compatible(${model})`,
|
|
35
110
|
model,
|
|
36
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Embed one or more texts.
|
|
113
|
+
* @param {string|string[]} texts - Text(s) to embed
|
|
114
|
+
* @param {number} [_attempt=0] - Internal retry counter
|
|
115
|
+
* @returns {Promise<number[][]>} Array of embedding vectors
|
|
116
|
+
* @throws {Error} On network failure, API error, or rate limit exceeded
|
|
117
|
+
*/
|
|
118
|
+
async embed(texts, _attempt = 0) {
|
|
37
119
|
const input = Array.isArray(texts) ? texts : [texts];
|
|
38
120
|
const res = await fetch(`${baseUrl}/embeddings`, {
|
|
39
121
|
method: 'POST',
|
|
@@ -41,8 +123,9 @@ export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.
|
|
|
41
123
|
body: JSON.stringify({ model, input, ...extraBody }),
|
|
42
124
|
});
|
|
43
125
|
if (res.status === 429) {
|
|
44
|
-
|
|
45
|
-
|
|
126
|
+
if (_attempt >= maxRetries) throw new Error(`Embedding 429: rate limited after ${maxRetries} retries`);
|
|
127
|
+
await new Promise(r => setTimeout(r, retryMs * (_attempt + 1)));
|
|
128
|
+
return this.embed(texts, _attempt + 1);
|
|
46
129
|
}
|
|
47
130
|
if (!res.ok) throw new Error(`Embedding ${res.status}: ${await res.text()}`);
|
|
48
131
|
const data = await res.json();
|
|
@@ -51,15 +134,31 @@ export function openaiEmbeddings({ apiKey, model, baseUrl = 'https://api.openai.
|
|
|
51
134
|
};
|
|
52
135
|
}
|
|
53
136
|
|
|
54
|
-
// ─── Noop Provider (keyword-only mode) ──────────────────────
|
|
55
137
|
/**
|
|
56
|
-
* No-op embedding provider
|
|
57
|
-
*
|
|
138
|
+
* No-op embedding provider for keyword-only search.
|
|
139
|
+
*
|
|
140
|
+
* Returns `null` embeddings, which disables semantic search and falls back
|
|
141
|
+
* to substring matching. Useful for:
|
|
142
|
+
* - Testing without API dependencies
|
|
143
|
+
* - Offline/airgapped environments
|
|
144
|
+
* - Simple exact-match use cases
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} [opts] - Options (currently unused, reserved for future)
|
|
147
|
+
* @returns {EmbeddingProvider} No-op provider
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const mem = createMemory({ embeddings: { type: 'noop' } });
|
|
151
|
+
* await mem.store('a', 'User prefers dark mode');
|
|
152
|
+
* const results = await mem.search('a', 'dark mode'); // Keyword match
|
|
58
153
|
*/
|
|
59
154
|
export function noopEmbeddings() {
|
|
60
155
|
return {
|
|
61
156
|
name: 'noop',
|
|
62
157
|
model: null,
|
|
158
|
+
/**
|
|
159
|
+
* @param {string|string[]} texts
|
|
160
|
+
* @returns {Promise<null[]>} Array of nulls
|
|
161
|
+
*/
|
|
63
162
|
async embed(texts) {
|
|
64
163
|
const input = Array.isArray(texts) ? texts : [texts];
|
|
65
164
|
return input.map(() => null);
|
package/src/extraction.mjs
CHANGED
|
@@ -1,22 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @module extraction
|
|
3
|
+
* @description Fact extraction providers for bulk ingestion.
|
|
4
|
+
*
|
|
5
|
+
* All extraction providers must implement:
|
|
6
|
+
* ```typescript
|
|
7
|
+
* interface ExtractionProvider {
|
|
8
|
+
* name: string;
|
|
9
|
+
* extract(text: string): Promise<Fact[]>;
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* interface Fact {
|
|
13
|
+
* fact: string;
|
|
14
|
+
* category: string;
|
|
15
|
+
* importance: number;
|
|
16
|
+
* tags: string[];
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // LLM extraction
|
|
22
|
+
* import { llmExtraction } from '@jeremiaheth/neolata-mem/extraction';
|
|
23
|
+
* const extractor = llmExtraction({
|
|
24
|
+
* apiKey: process.env.OPENAI_API_KEY,
|
|
25
|
+
* model: 'gpt-4.1-nano',
|
|
26
|
+
* });
|
|
27
|
+
* const facts = await extractor.extract('User deployed v2.1 with Redis caching on port 6379');
|
|
28
|
+
* // [{ fact: 'Deployed v2.1', category: 'event', importance: 0.7, tags: ['deployment'] }, ...]
|
|
5
29
|
*/
|
|
6
30
|
|
|
7
|
-
|
|
31
|
+
import { openaiChat } from './llm.mjs';
|
|
32
|
+
|
|
8
33
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @
|
|
11
|
-
* @
|
|
12
|
-
* @
|
|
13
|
-
* @
|
|
34
|
+
* @typedef {Object} Fact
|
|
35
|
+
* @property {string} fact - Atomic fact statement
|
|
36
|
+
* @property {string} category - Fact category (decision|finding|fact|insight|task|event|preference)
|
|
37
|
+
* @property {number} importance - Importance score 0.0-1.0
|
|
38
|
+
* @property {string[]} tags - Extracted keywords/tags
|
|
14
39
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} ExtractionProvider
|
|
43
|
+
* @property {string} name - Provider identifier
|
|
44
|
+
* @property {function(string): Promise<Fact[]>} extract - Extract facts from text
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* LLM prompt for structured fact extraction.
|
|
49
|
+
* @const {string}
|
|
50
|
+
* @private
|
|
51
|
+
*/
|
|
52
|
+
const EXTRACT_PROMPT = `You are a precise fact extractor. Extract discrete, atomic facts from the following text. Each fact should be self-contained and include specific details (names, numbers, dates, decisions, preferences).
|
|
20
53
|
|
|
21
54
|
Output as a JSON array of objects with fields:
|
|
22
55
|
- "fact": the extracted fact (string)
|
|
@@ -25,43 +58,122 @@ Output as a JSON array of objects with fields:
|
|
|
25
58
|
- "tags": array of relevant keywords
|
|
26
59
|
|
|
27
60
|
Text to extract from:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Respond ONLY with the JSON array, no markdown formatting.`;
|
|
61
|
+
`;
|
|
31
62
|
|
|
63
|
+
/**
|
|
64
|
+
* LLM-based fact extraction (OpenAI-compatible API).
|
|
65
|
+
*
|
|
66
|
+
* Sends text to an LLM with structured prompt to extract atomic facts.
|
|
67
|
+
* Falls back to single-fact passthrough on LLM failure.
|
|
68
|
+
*
|
|
69
|
+
* **Recommended models:**
|
|
70
|
+
* - `gpt-4.1-nano` (fast, cheap)
|
|
71
|
+
* - `gpt-4o-mini`
|
|
72
|
+
* - `claude-3-haiku` (if using Anthropic)
|
|
73
|
+
*
|
|
74
|
+
* **Cost:** ~$0.0001-0.001 per 1000 chars depending on model.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} opts - Configuration options
|
|
77
|
+
* @param {string} opts.apiKey - API key for OpenAI-compatible endpoint
|
|
78
|
+
* @param {string} [opts.model='gpt-4.1-nano'] - Chat model for extraction
|
|
79
|
+
* @param {string} [opts.baseUrl='https://api.openai.com/v1'] - API base URL
|
|
80
|
+
* @returns {ExtractionProvider} Configured extraction provider
|
|
81
|
+
* @throws {Error} If apiKey is missing
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // OpenAI
|
|
85
|
+
* const ext = llmExtraction({
|
|
86
|
+
* apiKey: process.env.OPENAI_API_KEY,
|
|
87
|
+
* model: 'gpt-4.1-nano',
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Groq (fast inference)
|
|
92
|
+
* const ext = llmExtraction({
|
|
93
|
+
* apiKey: process.env.GROQ_API_KEY,
|
|
94
|
+
* model: 'llama-3.3-70b-versatile',
|
|
95
|
+
* baseUrl: 'https://api.groq.com/openai/v1',
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* // Use with ingest
|
|
100
|
+
* const mem = createMemory({
|
|
101
|
+
* extraction: { type: 'llm', apiKey: KEY, model: 'gpt-4.1-nano' },
|
|
102
|
+
* });
|
|
103
|
+
* const result = await mem.ingest('agent', longDocument);
|
|
104
|
+
* // Extracted facts automatically stored with A-MEM linking
|
|
105
|
+
*/
|
|
106
|
+
export function llmExtraction({ apiKey, model = 'gpt-4.1-nano', baseUrl = 'https://api.openai.com/v1' }) {
|
|
107
|
+
if (!apiKey) throw new Error('llmExtraction: apiKey is required');
|
|
108
|
+
const chat = openaiChat({ apiKey, model, baseUrl, maxTokens: 2000, temperature: 0.1 });
|
|
109
|
+
return {
|
|
110
|
+
name: `llm(${model})`,
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract structured facts from text using LLM.
|
|
114
|
+
* @param {string} text - Input text (up to ~4000 tokens)
|
|
115
|
+
* @returns {Promise<Fact[]>} Array of extracted facts
|
|
116
|
+
* @throws Never throws — falls back to passthrough on failure
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* const facts = await extractor.extract(`
|
|
120
|
+
* Meeting notes 2024-02-22:
|
|
121
|
+
* - Decided to migrate to PostgreSQL
|
|
122
|
+
* - Redis will handle session cache on port 6379
|
|
123
|
+
* - Deploy v2.1 by Friday
|
|
124
|
+
* `);
|
|
125
|
+
* // [
|
|
126
|
+
* // { fact: 'Decided to migrate to PostgreSQL', category: 'decision', importance: 0.9, tags: ['postgresql', 'migration'] },
|
|
127
|
+
* // { fact: 'Redis handles session cache on port 6379', category: 'fact', importance: 0.7, tags: ['redis', 'cache'] },
|
|
128
|
+
* // { fact: 'Deploy v2.1 by Friday', category: 'task', importance: 0.8, tags: ['deployment', 'deadline'] },
|
|
129
|
+
* // ]
|
|
130
|
+
*/
|
|
131
|
+
async extract(text) {
|
|
32
132
|
try {
|
|
33
|
-
const
|
|
34
|
-
method: 'POST',
|
|
35
|
-
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
36
|
-
body: JSON.stringify({
|
|
37
|
-
model,
|
|
38
|
-
messages: [{ role: 'user', content: prompt }],
|
|
39
|
-
max_tokens: 2000,
|
|
40
|
-
temperature: 0.1,
|
|
41
|
-
}),
|
|
42
|
-
});
|
|
43
|
-
if (!res.ok) throw new Error(`LLM ${res.status}: ${await res.text()}`);
|
|
44
|
-
const data = await res.json();
|
|
45
|
-
const content = data.choices?.[0]?.message?.content || '';
|
|
133
|
+
const content = await chat.chat(`${EXTRACT_PROMPT}${text}\n\nRespond ONLY with the JSON array, no markdown formatting.`);
|
|
46
134
|
const jsonStr = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
|
47
135
|
return JSON.parse(jsonStr);
|
|
48
136
|
} catch (e) {
|
|
137
|
+
// Fallback: treat entire text as single fact
|
|
49
138
|
return [{ fact: text, category: 'fact', importance: 0.5, tags: [] }];
|
|
50
139
|
}
|
|
51
140
|
},
|
|
52
141
|
};
|
|
53
142
|
}
|
|
54
143
|
|
|
55
|
-
// ─── Passthrough Extraction (No LLM) ────────────────────────
|
|
56
144
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
145
|
+
* Passthrough extraction (no LLM, treats input as single fact).
|
|
146
|
+
*
|
|
147
|
+
* Wraps the entire input text as a single fact. Useful when:
|
|
148
|
+
* - Input is already atomic/structured
|
|
149
|
+
* - No LLM access (offline mode)
|
|
150
|
+
* - You want manual control over categories/importance
|
|
151
|
+
*
|
|
152
|
+
* @param {Object} [opts] - Configuration
|
|
153
|
+
* @param {string} [opts.defaultCategory='fact'] - Default category for all facts
|
|
154
|
+
* @param {number} [opts.defaultImportance=0.5] - Default importance (0.0-1.0)
|
|
155
|
+
* @returns {ExtractionProvider} Passthrough provider
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* const ext = passthroughExtraction({ defaultCategory: 'event', defaultImportance: 0.7 });
|
|
159
|
+
* const facts = await ext.extract('Deployed v2.1');
|
|
160
|
+
* // [{ fact: 'Deployed v2.1', category: 'event', importance: 0.7, tags: [] }]
|
|
161
|
+
*
|
|
162
|
+
* @example
|
|
163
|
+
* // Use with ingest when you control fact structure
|
|
164
|
+
* const mem = createMemory({
|
|
165
|
+
* extraction: { type: 'passthrough' },
|
|
166
|
+
* });
|
|
167
|
+
* await mem.ingest('agent', 'Server runs on port 8080'); // Single fact
|
|
61
168
|
*/
|
|
62
169
|
export function passthroughExtraction({ defaultCategory = 'fact', defaultImportance = 0.5 } = {}) {
|
|
63
170
|
return {
|
|
64
171
|
name: 'passthrough',
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} text
|
|
175
|
+
* @returns {Promise<Fact[]>}
|
|
176
|
+
*/
|
|
65
177
|
async extract(text) {
|
|
66
178
|
return [{ fact: text, category: defaultCategory, importance: defaultImportance, tags: [] }];
|
|
67
179
|
},
|