audrey 0.8.0 → 0.11.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 +76 -8
- package/mcp-server/config.js +30 -16
- package/mcp-server/index.js +43 -5
- package/package.json +2 -1
- package/src/affect.js +64 -0
- package/src/audrey.js +82 -6
- package/src/db.js +321 -281
- package/src/embedding.js +90 -53
- package/src/encode.js +63 -54
- package/src/export.js +5 -3
- package/src/import.js +15 -8
- package/src/index.js +1 -0
- package/src/migrate.js +27 -9
- package/src/prompts.js +43 -0
- package/src/recall.js +27 -16
package/src/embedding.js
CHANGED
|
@@ -11,33 +11,14 @@ import { createHash } from 'node:crypto';
|
|
|
11
11
|
* @property {(buffer: Buffer) => number[]} bufferToVector
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {Object} MockEmbeddingConfig
|
|
16
|
-
* @property {'mock'} provider
|
|
17
|
-
* @property {number} [dimensions=64]
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* @typedef {Object} OpenAIEmbeddingConfig
|
|
22
|
-
* @property {'openai'} provider
|
|
23
|
-
* @property {string} [apiKey]
|
|
24
|
-
* @property {string} [model='text-embedding-3-small']
|
|
25
|
-
* @property {number} [dimensions=1536]
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
14
|
/** @implements {EmbeddingProvider} */
|
|
29
15
|
export class MockEmbeddingProvider {
|
|
30
|
-
/** @param {Partial<MockEmbeddingConfig>} [config={}] */
|
|
31
16
|
constructor({ dimensions = 64 } = {}) {
|
|
32
17
|
this.dimensions = dimensions;
|
|
33
18
|
this.modelName = 'mock-embedding';
|
|
34
19
|
this.modelVersion = '1.0.0';
|
|
35
20
|
}
|
|
36
21
|
|
|
37
|
-
/**
|
|
38
|
-
* @param {string} text
|
|
39
|
-
* @returns {Promise<number[]>}
|
|
40
|
-
*/
|
|
41
22
|
async embed(text) {
|
|
42
23
|
const hash = createHash('sha256').update(text).digest();
|
|
43
24
|
const vector = new Array(this.dimensions);
|
|
@@ -48,26 +29,14 @@ export class MockEmbeddingProvider {
|
|
|
48
29
|
return vector.map(v => v / magnitude);
|
|
49
30
|
}
|
|
50
31
|
|
|
51
|
-
/**
|
|
52
|
-
* @param {string[]} texts
|
|
53
|
-
* @returns {Promise<number[][]>}
|
|
54
|
-
*/
|
|
55
32
|
async embedBatch(texts) {
|
|
56
33
|
return Promise.all(texts.map(t => this.embed(t)));
|
|
57
34
|
}
|
|
58
35
|
|
|
59
|
-
/**
|
|
60
|
-
* @param {number[]} vector
|
|
61
|
-
* @returns {Buffer}
|
|
62
|
-
*/
|
|
63
36
|
vectorToBuffer(vector) {
|
|
64
37
|
return Buffer.from(new Float32Array(vector).buffer);
|
|
65
38
|
}
|
|
66
39
|
|
|
67
|
-
/**
|
|
68
|
-
* @param {Buffer} buffer
|
|
69
|
-
* @returns {number[]}
|
|
70
|
-
*/
|
|
71
40
|
bufferToVector(buffer) {
|
|
72
41
|
return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
|
|
73
42
|
}
|
|
@@ -75,7 +44,6 @@ export class MockEmbeddingProvider {
|
|
|
75
44
|
|
|
76
45
|
/** @implements {EmbeddingProvider} */
|
|
77
46
|
export class OpenAIEmbeddingProvider {
|
|
78
|
-
/** @param {Partial<OpenAIEmbeddingConfig>} [config={}] */
|
|
79
47
|
constructor({ apiKey, model = 'text-embedding-3-small', dimensions = 1536, timeout = 30000 } = {}) {
|
|
80
48
|
this.apiKey = apiKey || process.env.OPENAI_API_KEY;
|
|
81
49
|
this.model = model;
|
|
@@ -85,10 +53,6 @@ export class OpenAIEmbeddingProvider {
|
|
|
85
53
|
this.modelVersion = 'latest';
|
|
86
54
|
}
|
|
87
55
|
|
|
88
|
-
/**
|
|
89
|
-
* @param {string} text
|
|
90
|
-
* @returns {Promise<number[]>}
|
|
91
|
-
*/
|
|
92
56
|
async embed(text) {
|
|
93
57
|
const controller = new AbortController();
|
|
94
58
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -110,10 +74,6 @@ export class OpenAIEmbeddingProvider {
|
|
|
110
74
|
}
|
|
111
75
|
}
|
|
112
76
|
|
|
113
|
-
/**
|
|
114
|
-
* @param {string[]} texts
|
|
115
|
-
* @returns {Promise<number[][]>}
|
|
116
|
-
*/
|
|
117
77
|
async embedBatch(texts) {
|
|
118
78
|
const controller = new AbortController();
|
|
119
79
|
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
@@ -135,34 +95,111 @@ export class OpenAIEmbeddingProvider {
|
|
|
135
95
|
}
|
|
136
96
|
}
|
|
137
97
|
|
|
138
|
-
/**
|
|
139
|
-
* @param {number[]} vector
|
|
140
|
-
* @returns {Buffer}
|
|
141
|
-
*/
|
|
142
98
|
vectorToBuffer(vector) {
|
|
143
99
|
return Buffer.from(new Float32Array(vector).buffer);
|
|
144
100
|
}
|
|
145
101
|
|
|
146
|
-
/**
|
|
147
|
-
* @param {Buffer} buffer
|
|
148
|
-
* @returns {number[]}
|
|
149
|
-
*/
|
|
150
102
|
bufferToVector(buffer) {
|
|
151
103
|
return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
|
|
152
104
|
}
|
|
153
105
|
}
|
|
154
106
|
|
|
155
|
-
/**
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
107
|
+
/** @implements {EmbeddingProvider} */
|
|
108
|
+
export class LocalEmbeddingProvider {
|
|
109
|
+
constructor({ model = 'Xenova/all-MiniLM-L6-v2' } = {}) {
|
|
110
|
+
this.model = model;
|
|
111
|
+
this.dimensions = 384;
|
|
112
|
+
this.modelName = model;
|
|
113
|
+
this.modelVersion = '1.0.0';
|
|
114
|
+
this._pipeline = null;
|
|
115
|
+
this._readyPromise = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ready() {
|
|
119
|
+
if (!this._readyPromise) {
|
|
120
|
+
this._readyPromise = import('@huggingface/transformers').then(({ pipeline }) =>
|
|
121
|
+
pipeline('feature-extraction', this.model, { dtype: 'fp32' })
|
|
122
|
+
).then(pipe => { this._pipeline = pipe; });
|
|
123
|
+
}
|
|
124
|
+
return this._readyPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async embed(text) {
|
|
128
|
+
await this.ready();
|
|
129
|
+
const output = await this._pipeline(text, { pooling: 'mean', normalize: true });
|
|
130
|
+
return Array.from(output.data);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async embedBatch(texts) {
|
|
134
|
+
return Promise.all(texts.map(t => this.embed(t)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
vectorToBuffer(vector) {
|
|
138
|
+
return Buffer.from(new Float32Array(vector).buffer);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
bufferToVector(buffer) {
|
|
142
|
+
return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @implements {EmbeddingProvider} */
|
|
147
|
+
export class GeminiEmbeddingProvider {
|
|
148
|
+
constructor({ apiKey, model = 'gemini-embedding-001', timeout = 30000 } = {}) {
|
|
149
|
+
this.apiKey = apiKey || process.env.GOOGLE_API_KEY;
|
|
150
|
+
this.model = model;
|
|
151
|
+
this.dimensions = 3072;
|
|
152
|
+
this.timeout = timeout;
|
|
153
|
+
this.modelName = model;
|
|
154
|
+
this.modelVersion = 'latest';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async embed(text) {
|
|
158
|
+
if (!this.apiKey) throw new Error('Gemini embedding requires GOOGLE_API_KEY');
|
|
159
|
+
const controller = new AbortController();
|
|
160
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
161
|
+
try {
|
|
162
|
+
const response = await fetch(
|
|
163
|
+
`https://generativelanguage.googleapis.com/v1beta/models/${this.model}:embedContent?key=${this.apiKey}`,
|
|
164
|
+
{
|
|
165
|
+
method: 'POST',
|
|
166
|
+
headers: { 'Content-Type': 'application/json' },
|
|
167
|
+
body: JSON.stringify({ model: `models/${this.model}`, content: { parts: [{ text }] } }),
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
if (!response.ok) throw new Error(`Gemini embedding failed: ${response.status}`);
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
return data.embedding.values;
|
|
174
|
+
} finally {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async embedBatch(texts) {
|
|
180
|
+
return Promise.all(texts.map(t => this.embed(t)));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
vectorToBuffer(vector) {
|
|
184
|
+
return Buffer.from(new Float32Array(vector).buffer);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
bufferToVector(buffer) {
|
|
188
|
+
return Array.from(new Float32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / 4));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
159
192
|
export function createEmbeddingProvider(config) {
|
|
160
193
|
switch (config.provider) {
|
|
161
194
|
case 'mock':
|
|
162
195
|
return new MockEmbeddingProvider(config);
|
|
163
196
|
case 'openai':
|
|
164
197
|
return new OpenAIEmbeddingProvider(config);
|
|
198
|
+
case 'local':
|
|
199
|
+
return new LocalEmbeddingProvider(config);
|
|
200
|
+
case 'gemini':
|
|
201
|
+
return new GeminiEmbeddingProvider(config);
|
|
165
202
|
default:
|
|
166
|
-
throw new Error(`Unknown embedding provider: ${config.provider}. Valid: mock, openai`);
|
|
203
|
+
throw new Error(`Unknown embedding provider: ${config.provider}. Valid: mock, openai, local, gemini`);
|
|
167
204
|
}
|
|
168
205
|
}
|
package/src/encode.js
CHANGED
|
@@ -1,54 +1,63 @@
|
|
|
1
|
-
import { generateId } from './ulid.js';
|
|
2
|
-
import { sourceReliability } from './confidence.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* @param {import('
|
|
7
|
-
* @param {
|
|
8
|
-
* @
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
1
|
+
import { generateId } from './ulid.js';
|
|
2
|
+
import { sourceReliability } from './confidence.js';
|
|
3
|
+
import { arousalSalienceBoost } from './affect.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {import('better-sqlite3').Database} db
|
|
7
|
+
* @param {import('./embedding.js').EmbeddingProvider} embeddingProvider
|
|
8
|
+
* @param {{ content: string, source: string, salience?: number, causal?: { trigger?: string, consequence?: string }, tags?: string[], supersedes?: string, context?: object, affect?: object, arousalWeight?: number, private?: boolean }} params
|
|
9
|
+
* @returns {Promise<string>}
|
|
10
|
+
*/
|
|
11
|
+
export async function encodeEpisode(db, embeddingProvider, {
|
|
12
|
+
content,
|
|
13
|
+
source,
|
|
14
|
+
salience = 0.5,
|
|
15
|
+
causal,
|
|
16
|
+
tags,
|
|
17
|
+
supersedes,
|
|
18
|
+
context = {},
|
|
19
|
+
affect = {},
|
|
20
|
+
arousalWeight = 0.3,
|
|
21
|
+
private: isPrivate = false,
|
|
22
|
+
}) {
|
|
23
|
+
if (!content || typeof content !== 'string') throw new Error('content must be a non-empty string');
|
|
24
|
+
if (salience < 0 || salience > 1) throw new Error('salience must be between 0 and 1');
|
|
25
|
+
if (tags && !Array.isArray(tags)) throw new Error('tags must be an array');
|
|
26
|
+
|
|
27
|
+
const reliability = sourceReliability(source);
|
|
28
|
+
const vector = await embeddingProvider.embed(content);
|
|
29
|
+
const embeddingBuffer = embeddingProvider.vectorToBuffer(vector);
|
|
30
|
+
const id = generateId();
|
|
31
|
+
const now = new Date().toISOString();
|
|
32
|
+
|
|
33
|
+
const boost = arousalSalienceBoost(affect.arousal);
|
|
34
|
+
const effectiveSalience = Math.min(1.0, salience + (boost * arousalWeight));
|
|
35
|
+
|
|
36
|
+
const insertAndLink = db.transaction(() => {
|
|
37
|
+
db.prepare(`
|
|
38
|
+
INSERT INTO episodes (
|
|
39
|
+
id, content, embedding, source, source_reliability, salience, context, affect,
|
|
40
|
+
tags, causal_trigger, causal_consequence, created_at,
|
|
41
|
+
embedding_model, embedding_version, supersedes, "private"
|
|
42
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
43
|
+
`).run(
|
|
44
|
+
id, content, embeddingBuffer, source, reliability, effectiveSalience,
|
|
45
|
+
JSON.stringify(context),
|
|
46
|
+
JSON.stringify(affect),
|
|
47
|
+
tags ? JSON.stringify(tags) : null,
|
|
48
|
+
causal?.trigger || null, causal?.consequence || null,
|
|
49
|
+
now, embeddingProvider.modelName, embeddingProvider.modelVersion,
|
|
50
|
+
supersedes || null,
|
|
51
|
+
isPrivate ? 1 : 0,
|
|
52
|
+
);
|
|
53
|
+
db.prepare(
|
|
54
|
+
'INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)'
|
|
55
|
+
).run(id, embeddingBuffer, source, BigInt(0));
|
|
56
|
+
if (supersedes) {
|
|
57
|
+
db.prepare('UPDATE episodes SET superseded_by = ? WHERE id = ?').run(id, supersedes);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
insertAndLink();
|
|
62
|
+
return id;
|
|
63
|
+
}
|
package/src/export.js
CHANGED
|
@@ -8,21 +8,23 @@ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')
|
|
|
8
8
|
|
|
9
9
|
export function exportMemories(db) {
|
|
10
10
|
const episodes = db.prepare(
|
|
11
|
-
'SELECT id, content, source, source_reliability, salience, tags, causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated FROM episodes'
|
|
11
|
+
'SELECT id, content, source, source_reliability, salience, context, affect, tags, causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated, "private" FROM episodes'
|
|
12
12
|
).all().map(ep => ({
|
|
13
13
|
...ep,
|
|
14
14
|
tags: safeJsonParse(ep.tags, null),
|
|
15
|
+
context: safeJsonParse(ep.context, null),
|
|
16
|
+
affect: safeJsonParse(ep.affect, null),
|
|
15
17
|
}));
|
|
16
18
|
|
|
17
19
|
const semantics = db.prepare(
|
|
18
|
-
'SELECT id, content, state, conditions, evidence_episode_ids, evidence_count, supporting_count, contradicting_count, source_type_diversity, consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count FROM semantics'
|
|
20
|
+
'SELECT id, content, state, conditions, evidence_episode_ids, evidence_count, supporting_count, contradicting_count, source_type_diversity, consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count, interference_count, salience FROM semantics'
|
|
19
21
|
).all().map(sem => ({
|
|
20
22
|
...sem,
|
|
21
23
|
evidence_episode_ids: safeJsonParse(sem.evidence_episode_ids, []),
|
|
22
24
|
}));
|
|
23
25
|
|
|
24
26
|
const procedures = db.prepare(
|
|
25
|
-
'SELECT id, content, state, trigger_conditions, evidence_episode_ids, success_count, failure_count, created_at, last_reinforced_at, retrieval_count FROM procedures'
|
|
27
|
+
'SELECT id, content, state, trigger_conditions, evidence_episode_ids, success_count, failure_count, created_at, last_reinforced_at, retrieval_count, interference_count, salience FROM procedures'
|
|
26
28
|
).all().map(proc => ({
|
|
27
29
|
...proc,
|
|
28
30
|
evidence_episode_ids: safeJsonParse(proc.evidence_episode_ids, []),
|
package/src/import.js
CHANGED
|
@@ -5,9 +5,9 @@ export async function importMemories(db, embeddingProvider, snapshot) {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
const insertEpisode = db.prepare(`
|
|
8
|
-
INSERT INTO episodes (id, content, source, source_reliability, salience, tags,
|
|
9
|
-
causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated)
|
|
10
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8
|
+
INSERT INTO episodes (id, content, source, source_reliability, salience, context, affect, tags,
|
|
9
|
+
causal_trigger, causal_consequence, created_at, supersedes, superseded_by, consolidated, "private")
|
|
10
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
11
11
|
`);
|
|
12
12
|
|
|
13
13
|
const insertVecEpisode = db.prepare(
|
|
@@ -17,8 +17,9 @@ export async function importMemories(db, embeddingProvider, snapshot) {
|
|
|
17
17
|
const insertSemantic = db.prepare(`
|
|
18
18
|
INSERT INTO semantics (id, content, state, conditions, evidence_episode_ids,
|
|
19
19
|
evidence_count, supporting_count, contradicting_count, source_type_diversity,
|
|
20
|
-
consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count
|
|
21
|
-
|
|
20
|
+
consolidation_checkpoint, created_at, last_reinforced_at, retrieval_count, challenge_count,
|
|
21
|
+
interference_count, salience)
|
|
22
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
22
23
|
`);
|
|
23
24
|
|
|
24
25
|
const insertVecSemantic = db.prepare(
|
|
@@ -27,8 +28,9 @@ export async function importMemories(db, embeddingProvider, snapshot) {
|
|
|
27
28
|
|
|
28
29
|
const insertProcedure = db.prepare(`
|
|
29
30
|
INSERT INTO procedures (id, content, state, trigger_conditions, evidence_episode_ids,
|
|
30
|
-
success_count, failure_count, created_at, last_reinforced_at, retrieval_count
|
|
31
|
-
|
|
31
|
+
success_count, failure_count, created_at, last_reinforced_at, retrieval_count,
|
|
32
|
+
interference_count, salience)
|
|
33
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
32
34
|
`);
|
|
33
35
|
|
|
34
36
|
const insertVecProcedure = db.prepare(
|
|
@@ -53,10 +55,13 @@ export async function importMemories(db, embeddingProvider, snapshot) {
|
|
|
53
55
|
|
|
54
56
|
for (const ep of snapshot.episodes) {
|
|
55
57
|
const tags = ep.tags ? JSON.stringify(ep.tags) : null;
|
|
58
|
+
const context = ep.context ? JSON.stringify(ep.context) : '{}';
|
|
59
|
+
const affect = ep.affect ? JSON.stringify(ep.affect) : '{}';
|
|
56
60
|
insertEpisode.run(
|
|
57
61
|
ep.id, ep.content, ep.source, ep.source_reliability, ep.salience ?? 0.5,
|
|
58
|
-
tags, ep.causal_trigger ?? null, ep.causal_consequence ?? null,
|
|
62
|
+
context, affect, tags, ep.causal_trigger ?? null, ep.causal_consequence ?? null,
|
|
59
63
|
ep.created_at, ep.supersedes ?? null, ep.superseded_by ?? null, ep.consolidated ?? 0,
|
|
64
|
+
ep.private ?? 0,
|
|
60
65
|
);
|
|
61
66
|
|
|
62
67
|
const vector = await embeddingProvider.embed(ep.content);
|
|
@@ -71,6 +76,7 @@ export async function importMemories(db, embeddingProvider, snapshot) {
|
|
|
71
76
|
sem.evidence_count ?? 0, sem.supporting_count ?? 0, sem.contradicting_count ?? 0,
|
|
72
77
|
sem.source_type_diversity ?? 0, sem.consolidation_checkpoint ?? null,
|
|
73
78
|
sem.created_at, sem.last_reinforced_at ?? null, sem.retrieval_count ?? 0, sem.challenge_count ?? 0,
|
|
79
|
+
sem.interference_count ?? 0, sem.salience ?? 0.5,
|
|
74
80
|
);
|
|
75
81
|
|
|
76
82
|
const vector = await embeddingProvider.embed(sem.content);
|
|
@@ -84,6 +90,7 @@ export async function importMemories(db, embeddingProvider, snapshot) {
|
|
|
84
90
|
JSON.stringify(proc.evidence_episode_ids || []),
|
|
85
91
|
proc.success_count ?? 0, proc.failure_count ?? 0,
|
|
86
92
|
proc.created_at, proc.last_reinforced_at ?? null, proc.retrieval_count ?? 0,
|
|
93
|
+
proc.interference_count ?? 0, proc.salience ?? 0.5,
|
|
87
94
|
);
|
|
88
95
|
|
|
89
96
|
const vector = await embeddingProvider.embed(proc.content);
|
package/src/index.js
CHANGED
|
@@ -17,3 +17,4 @@ export { reembedAll } from './migrate.js';
|
|
|
17
17
|
export { forgetMemory, forgetByQuery, purgeMemories } from './forget.js';
|
|
18
18
|
export { applyInterference, interferenceModifier } from './interference.js';
|
|
19
19
|
export { contextMatchRatio, contextModifier } from './context.js';
|
|
20
|
+
export { arousalSalienceBoost, affectSimilarity, moodCongruenceModifier, detectResonance } from './affect.js';
|
package/src/migrate.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { dropVec0Tables, createVec0Tables } from './db.js';
|
|
2
|
+
|
|
3
|
+
export async function reembedAll(db, embeddingProvider, { dropAndRecreate = false } = {}) {
|
|
4
|
+
if (dropAndRecreate) {
|
|
5
|
+
dropVec0Tables(db);
|
|
6
|
+
createVec0Tables(db, embeddingProvider.dimensions);
|
|
7
|
+
}
|
|
8
|
+
|
|
2
9
|
const episodes = db.prepare('SELECT id, content, source FROM episodes').all();
|
|
3
10
|
const semantics = db.prepare('SELECT id, content, state FROM semantics').all();
|
|
4
11
|
const procedures = db.prepare('SELECT id, content, state FROM procedures').all();
|
|
@@ -7,26 +14,37 @@ export async function reembedAll(db, embeddingProvider) {
|
|
|
7
14
|
const vector = await embeddingProvider.embed(ep.content);
|
|
8
15
|
const buffer = embeddingProvider.vectorToBuffer(vector);
|
|
9
16
|
db.prepare('UPDATE episodes SET embedding = ? WHERE id = ?').run(buffer, ep.id);
|
|
10
|
-
db.prepare('
|
|
17
|
+
const exists = db.prepare('SELECT id FROM vec_episodes WHERE id = ?').get(ep.id);
|
|
18
|
+
if (!exists) {
|
|
19
|
+
db.prepare('INSERT INTO vec_episodes(id, embedding, source, consolidated) VALUES (?, ?, ?, ?)').run(ep.id, buffer, ep.source, BigInt(0));
|
|
20
|
+
} else {
|
|
21
|
+
db.prepare('UPDATE vec_episodes SET embedding = ? WHERE id = ?').run(buffer, ep.id);
|
|
22
|
+
}
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
for (const sem of semantics) {
|
|
14
26
|
const vector = await embeddingProvider.embed(sem.content);
|
|
15
27
|
const buffer = embeddingProvider.vectorToBuffer(vector);
|
|
16
28
|
db.prepare('UPDATE semantics SET embedding = ? WHERE id = ?').run(buffer, sem.id);
|
|
17
|
-
db.prepare('
|
|
29
|
+
const exists = db.prepare('SELECT id FROM vec_semantics WHERE id = ?').get(sem.id);
|
|
30
|
+
if (!exists) {
|
|
31
|
+
db.prepare('INSERT INTO vec_semantics(id, embedding, state) VALUES (?, ?, ?)').run(sem.id, buffer, sem.state);
|
|
32
|
+
} else {
|
|
33
|
+
db.prepare('UPDATE vec_semantics SET embedding = ? WHERE id = ?').run(buffer, sem.id);
|
|
34
|
+
}
|
|
18
35
|
}
|
|
19
36
|
|
|
20
37
|
for (const proc of procedures) {
|
|
21
38
|
const vector = await embeddingProvider.embed(proc.content);
|
|
22
39
|
const buffer = embeddingProvider.vectorToBuffer(vector);
|
|
23
40
|
db.prepare('UPDATE procedures SET embedding = ? WHERE id = ?').run(buffer, proc.id);
|
|
24
|
-
db.prepare('
|
|
41
|
+
const exists = db.prepare('SELECT id FROM vec_procedures WHERE id = ?').get(proc.id);
|
|
42
|
+
if (!exists) {
|
|
43
|
+
db.prepare('INSERT INTO vec_procedures(id, embedding, state) VALUES (?, ?, ?)').run(proc.id, buffer, proc.state);
|
|
44
|
+
} else {
|
|
45
|
+
db.prepare('UPDATE vec_procedures SET embedding = ? WHERE id = ?').run(buffer, proc.id);
|
|
46
|
+
}
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
return {
|
|
28
|
-
episodes: episodes.length,
|
|
29
|
-
semantics: semantics.length,
|
|
30
|
-
procedures: procedures.length,
|
|
31
|
-
};
|
|
49
|
+
return { episodes: episodes.length, semantics: semantics.length, procedures: procedures.length };
|
|
32
50
|
}
|
package/src/prompts.js
CHANGED
|
@@ -152,3 +152,46 @@ CLAIM B: ${claimB}${contextSection}`,
|
|
|
152
152
|
},
|
|
153
153
|
];
|
|
154
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {{ role: string, content: string }[]} turns
|
|
158
|
+
* @returns {import('./llm.js').ChatMessage[]}
|
|
159
|
+
*/
|
|
160
|
+
export function buildReflectionPrompt(turns) {
|
|
161
|
+
const transcript = turns.map(t => `${t.role.toUpperCase()}: ${t.content}`).join('\n\n');
|
|
162
|
+
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
role: 'system',
|
|
166
|
+
content: `You are performing memoryReflection. Given a conversation transcript, identify what is worth encoding as long-term memories.
|
|
167
|
+
|
|
168
|
+
Respond with ONLY valid JSON in this exact format:
|
|
169
|
+
{
|
|
170
|
+
"memories": [
|
|
171
|
+
{
|
|
172
|
+
"content": "The memory to encode — a clear, self-contained statement",
|
|
173
|
+
"source": "direct-observation" or "told-by-user" or "inference",
|
|
174
|
+
"salience": 0.0 to 1.0,
|
|
175
|
+
"tags": ["tag1", "tag2"],
|
|
176
|
+
"private": true or false,
|
|
177
|
+
"affect": { "valence": -1 to 1, "arousal": 0 to 1, "label": "emotion label" } or null
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
Rules:
|
|
183
|
+
- Encode facts about the user, decisions made, things that shifted
|
|
184
|
+
- Mark private: true for AI self-observations, emotional reactions, things felt but not said
|
|
185
|
+
- Mark private: false for facts about the user and project context
|
|
186
|
+
- Omit trivial exchanges — only encode what would matter in a future session
|
|
187
|
+
- Salience: 1.0 = extremely important, 0.5 = useful, 0.3 = background context
|
|
188
|
+
- Return empty memories array if nothing is worth encoding`,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
role: 'user',
|
|
192
|
+
content: turns.length > 0
|
|
193
|
+
? `Reflect on this conversation and identify what to encode:\n\n${transcript}`
|
|
194
|
+
: 'No conversation turns to reflect on.',
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
}
|
package/src/recall.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { computeConfidence, DEFAULT_HALF_LIVES, salienceModifier } from './confidence.js';
|
|
2
2
|
import { interferenceModifier } from './interference.js';
|
|
3
3
|
import { contextMatchRatio, contextModifier } from './context.js';
|
|
4
|
+
import { moodCongruenceModifier, affectSimilarity } from './affect.js';
|
|
4
5
|
import { daysBetween, safeJsonParse } from './utils.js';
|
|
5
6
|
|
|
6
7
|
function computeEpisodicConfidence(ep, now, confidenceConfig = {}) {
|
|
@@ -65,7 +66,7 @@ function computeProceduralConfidence(proc, now, confidenceConfig = {}) {
|
|
|
65
66
|
return Math.max(0, Math.min(1, confidence));
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch) {
|
|
69
|
+
function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMatch, moodCongruence) {
|
|
69
70
|
const entry = {
|
|
70
71
|
id: ep.id,
|
|
71
72
|
content: ep.content,
|
|
@@ -78,6 +79,9 @@ function buildEpisodicEntry(ep, confidence, score, includeProvenance, contextMat
|
|
|
78
79
|
if (contextMatch !== undefined) {
|
|
79
80
|
entry.contextMatch = contextMatch;
|
|
80
81
|
}
|
|
82
|
+
if (moodCongruence !== undefined) {
|
|
83
|
+
entry.moodCongruence = moodCongruence;
|
|
84
|
+
}
|
|
81
85
|
if (includeProvenance) {
|
|
82
86
|
entry.provenance = {
|
|
83
87
|
source: ep.source,
|
|
@@ -146,7 +150,8 @@ function matchesDateFilters(createdAt, filters) {
|
|
|
146
150
|
return true;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
|
-
function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}) {
|
|
153
|
+
function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters = {}, includePrivate = false) {
|
|
154
|
+
const privateClause = includePrivate ? '' : 'AND e."private" = 0';
|
|
150
155
|
const rows = db.prepare(`
|
|
151
156
|
SELECT e.*, (1.0 - v.distance) AS similarity
|
|
152
157
|
FROM vec_episodes v
|
|
@@ -154,6 +159,7 @@ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includePro
|
|
|
154
159
|
WHERE v.embedding MATCH ?
|
|
155
160
|
AND k = ?
|
|
156
161
|
AND e.superseded_by IS NULL
|
|
162
|
+
${privateClause}
|
|
157
163
|
`).all(queryBuffer, candidateK);
|
|
158
164
|
|
|
159
165
|
const results = [];
|
|
@@ -174,9 +180,17 @@ function knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includePro
|
|
|
174
180
|
confidence = Math.max(0, Math.min(1, confidence));
|
|
175
181
|
}
|
|
176
182
|
|
|
183
|
+
let moodMatch;
|
|
184
|
+
if (confidenceConfig?.retrievalMood) {
|
|
185
|
+
const encodingAffect = safeJsonParse(row.affect, {});
|
|
186
|
+
moodMatch = affectSimilarity(encodingAffect, confidenceConfig.retrievalMood);
|
|
187
|
+
confidence *= moodCongruenceModifier(encodingAffect, confidenceConfig.retrievalMood, confidenceConfig.affectWeight);
|
|
188
|
+
confidence = Math.max(0, Math.min(1, confidence));
|
|
189
|
+
}
|
|
190
|
+
|
|
177
191
|
if (confidence < minConfidence) continue;
|
|
178
192
|
const score = row.similarity * confidence;
|
|
179
|
-
results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch));
|
|
193
|
+
results.push(buildEpisodicEntry(row, confidence, score, includeProvenance, ctxMatch, moodMatch));
|
|
180
194
|
}
|
|
181
195
|
return results;
|
|
182
196
|
}
|
|
@@ -246,6 +260,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
246
260
|
sources,
|
|
247
261
|
after,
|
|
248
262
|
before,
|
|
263
|
+
includePrivate = false,
|
|
249
264
|
} = options;
|
|
250
265
|
|
|
251
266
|
const queryVector = await embeddingProvider.embed(query);
|
|
@@ -259,7 +274,7 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
259
274
|
const allResults = [];
|
|
260
275
|
|
|
261
276
|
if (searchTypes.includes('episodic')) {
|
|
262
|
-
const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters);
|
|
277
|
+
const episodic = knnEpisodic(db, queryBuffer, candidateK, now, minConfidence, includeProvenance, confidenceConfig, filters, includePrivate);
|
|
263
278
|
allResults.push(...episodic);
|
|
264
279
|
}
|
|
265
280
|
|
|
@@ -269,13 +284,11 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
269
284
|
allResults.push(...semResults);
|
|
270
285
|
|
|
271
286
|
if (semIds.length > 0) {
|
|
272
|
-
const updateStmt = db.prepare(
|
|
273
|
-
'UPDATE semantics SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id = ?'
|
|
274
|
-
);
|
|
275
287
|
const nowISO = now.toISOString();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
288
|
+
const placeholders = semIds.map(() => '?').join(',');
|
|
289
|
+
db.prepare(
|
|
290
|
+
`UPDATE semantics SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id IN (${placeholders})`
|
|
291
|
+
).run(nowISO, ...semIds);
|
|
279
292
|
}
|
|
280
293
|
}
|
|
281
294
|
|
|
@@ -285,13 +298,11 @@ export async function* recallStream(db, embeddingProvider, query, options = {})
|
|
|
285
298
|
allResults.push(...procResults);
|
|
286
299
|
|
|
287
300
|
if (procIds.length > 0) {
|
|
288
|
-
const updateStmt = db.prepare(
|
|
289
|
-
'UPDATE procedures SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id = ?'
|
|
290
|
-
);
|
|
291
301
|
const nowISO = now.toISOString();
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
302
|
+
const placeholders = procIds.map(() => '?').join(',');
|
|
303
|
+
db.prepare(
|
|
304
|
+
`UPDATE procedures SET retrieval_count = retrieval_count + 1, last_reinforced_at = ? WHERE id IN (${placeholders})`
|
|
305
|
+
).run(nowISO, ...procIds);
|
|
295
306
|
}
|
|
296
307
|
}
|
|
297
308
|
|