cozo-memory 1.2.0 → 1.2.2

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.
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZettelkastenEvolutionService = void 0;
4
+ class ZettelkastenEvolutionService {
5
+ db;
6
+ embeddings;
7
+ config;
8
+ constructor(db, embeddings, config = {}) {
9
+ this.db = db;
10
+ this.embeddings = embeddings;
11
+ this.config = {
12
+ enableEvolution: true,
13
+ similarityThreshold: 0.7,
14
+ maxRelatedNotes: 5,
15
+ minKeywordFrequency: 2,
16
+ autoExtractKeywords: true,
17
+ autoBidirectionalLinks: true,
18
+ enrichmentDepth: 'shallow',
19
+ ...config
20
+ };
21
+ }
22
+ extractKeywords(text, minLength = 4) {
23
+ const stopWords = new Set([
24
+ 'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but',
25
+ 'in', 'with', 'to', 'for', 'of', 'as', 'by', 'this', 'that', 'it',
26
+ 'from', 'are', 'was', 'were', 'been', 'be', 'have', 'has', 'had',
27
+ 'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can',
28
+ 'may', 'might', 'must', 'shall'
29
+ ]);
30
+ const words = text
31
+ .toLowerCase()
32
+ .replace(/[^\w\s]/g, ' ')
33
+ .split(/\s+/)
34
+ .filter(word => word.length >= minLength &&
35
+ !stopWords.has(word) &&
36
+ !/^\d+$/.test(word));
37
+ const frequencies = new Map();
38
+ for (const word of words) {
39
+ frequencies.set(word, (frequencies.get(word) || 0) + 1);
40
+ }
41
+ return Array.from(frequencies.entries())
42
+ .filter(([_, count]) => count >= this.config.minKeywordFrequency)
43
+ .sort((a, b) => b[1] - a[1])
44
+ .map(([word, _]) => word)
45
+ .slice(0, 10);
46
+ }
47
+ extractTags(text) {
48
+ const tags = [];
49
+ const hashtagMatches = text.match(/#\w+/g);
50
+ if (hashtagMatches) {
51
+ tags.push(...hashtagMatches.map(tag => tag.substring(1).toLowerCase()));
52
+ }
53
+ const patternMatches = text.match(/(?:category|type|topic|subject):\s*(\w+)/gi);
54
+ if (patternMatches) {
55
+ tags.push(...patternMatches.map(match => {
56
+ const parts = match.split(':');
57
+ return parts[1].trim().toLowerCase();
58
+ }));
59
+ }
60
+ return [...new Set(tags)];
61
+ }
62
+ async findRelatedNotes(observationId, observationText, observationEmbedding) {
63
+ try {
64
+ const result = await this.db.run(`
65
+ ?[id, entity_id, text, similarity] :=
66
+ ~observation:semantic{id |
67
+ query: vec($embedding),
68
+ k: $k,
69
+ ef: 100,
70
+ bind_distance: dist
71
+ },
72
+ *observation{id, entity_id, text, @ "NOW"},
73
+ id != $exclude_id,
74
+ similarity = 1.0 - dist
75
+ `, {
76
+ embedding: observationEmbedding,
77
+ k: this.config.maxRelatedNotes + 5,
78
+ exclude_id: observationId
79
+ });
80
+ const relatedNotes = [];
81
+ const observationKeywords = this.extractKeywords(observationText);
82
+ for (const [id, entity_id, text, similarity] of result.rows) {
83
+ const sim = similarity;
84
+ if (sim < this.config.similarityThreshold) {
85
+ continue;
86
+ }
87
+ const noteKeywords = this.extractKeywords(text);
88
+ const sharedKeywords = observationKeywords.filter(k => noteKeywords.includes(k));
89
+ let connectionType = 'semantic';
90
+ let reason = `High semantic similarity (${sim.toFixed(3)})`;
91
+ if (sharedKeywords.length >= 2) {
92
+ connectionType = 'keyword';
93
+ reason = `Shared keywords: ${sharedKeywords.slice(0, 3).join(', ')}`;
94
+ }
95
+ relatedNotes.push({
96
+ observationId: id,
97
+ entityId: entity_id,
98
+ text: text,
99
+ similarity: sim,
100
+ sharedKeywords,
101
+ connectionType,
102
+ reason
103
+ });
104
+ if (relatedNotes.length >= this.config.maxRelatedNotes) {
105
+ break;
106
+ }
107
+ }
108
+ return relatedNotes;
109
+ }
110
+ catch (error) {
111
+ console.error('[ZettelkastenEvolution] Error finding related notes:', error);
112
+ return [];
113
+ }
114
+ }
115
+ async enrichObservation(observationId, observationText, observationEmbedding, entityId) {
116
+ if (!this.config.enableEvolution) {
117
+ return {
118
+ observationId,
119
+ relatedNotes: [],
120
+ extractedKeywords: [],
121
+ addedTags: [],
122
+ createdLinks: 0,
123
+ updatedMetadata: {},
124
+ evolutionSummary: 'Evolution disabled'
125
+ };
126
+ }
127
+ try {
128
+ const extractedKeywords = this.config.autoExtractKeywords
129
+ ? this.extractKeywords(observationText)
130
+ : [];
131
+ const addedTags = this.extractTags(observationText);
132
+ const relatedNotes = await this.findRelatedNotes(observationId, observationText, observationEmbedding);
133
+ let createdLinks = 0;
134
+ if (this.config.autoBidirectionalLinks) {
135
+ const now = Date.now() * 1000; // CozoDB Validity uses microseconds
136
+ for (const related of relatedNotes) {
137
+ try {
138
+ await this.db.run(`
139
+ ?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[
140
+ $from_id,
141
+ $to_id,
142
+ 'zettelkasten_link',
143
+ [${now}, true],
144
+ $strength,
145
+ {
146
+ "connection_type": $connection_type,
147
+ "shared_keywords": $shared_keywords,
148
+ "reason": $reason,
149
+ "auto_generated": true
150
+ }
151
+ ]]
152
+ :put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
153
+ `, {
154
+ from_id: observationId,
155
+ to_id: related.observationId,
156
+ strength: related.similarity,
157
+ connection_type: related.connectionType,
158
+ shared_keywords: JSON.stringify(related.sharedKeywords),
159
+ reason: related.reason
160
+ });
161
+ await this.db.run(`
162
+ ?[from_id, to_id, relation_type, created_at, strength, metadata] <- [[
163
+ $from_id,
164
+ $to_id,
165
+ 'zettelkasten_link',
166
+ [${now}, true],
167
+ $strength,
168
+ {
169
+ "connection_type": $connection_type,
170
+ "shared_keywords": $shared_keywords,
171
+ "reason": $reason,
172
+ "auto_generated": true,
173
+ "bidirectional": true
174
+ }
175
+ ]]
176
+ :put relationship {from_id, to_id, relation_type, created_at => strength, metadata}
177
+ `, {
178
+ from_id: related.observationId,
179
+ to_id: observationId,
180
+ strength: related.similarity,
181
+ connection_type: related.connectionType,
182
+ shared_keywords: JSON.stringify(related.sharedKeywords),
183
+ reason: related.reason
184
+ });
185
+ createdLinks += 2;
186
+ }
187
+ catch (error) {
188
+ console.error(`[ZettelkastenEvolution] Error creating link:`, error);
189
+ }
190
+ }
191
+ }
192
+ // Update metadata of related observations
193
+ for (const related of relatedNotes) {
194
+ try {
195
+ const metaResult = await this.db.run(`
196
+ ?[id, metadata] := *observation{id, metadata, @ "NOW"}, id = $id
197
+ `, { id: related.observationId });
198
+ if (metaResult.rows.length > 0) {
199
+ const currentMetadata = metaResult.rows[0][1] || {};
200
+ const existingKeywords = currentMetadata.zettelkasten_keywords || [];
201
+ const enrichedKeywords = [...new Set([...existingKeywords, ...extractedKeywords])];
202
+ const existingTags = currentMetadata.zettelkasten_tags || [];
203
+ const enrichedTags = [...new Set([...existingTags, ...addedTags])];
204
+ const existingRelated = currentMetadata.zettelkasten_related || [];
205
+ const enrichedRelated = [...new Set([...existingRelated, observationId])];
206
+ const mergedMetadata = {
207
+ ...currentMetadata,
208
+ zettelkasten_keywords: enrichedKeywords,
209
+ zettelkasten_tags: enrichedTags,
210
+ zettelkasten_related: enrichedRelated,
211
+ zettelkasten_last_enriched: Date.now()
212
+ };
213
+ await this.db.run(`
214
+ ?[id, created_at, entity_id, session_id, task_id, text, embedding, metadata] :=
215
+ *observation{id, created_at, entity_id, session_id, task_id, text, embedding, @ "NOW"},
216
+ id = $id,
217
+ metadata = $new_metadata
218
+
219
+ :put observation {id, created_at => entity_id, session_id, task_id, text, embedding, metadata}
220
+ `, {
221
+ id: related.observationId,
222
+ new_metadata: mergedMetadata
223
+ });
224
+ }
225
+ }
226
+ catch (error) {
227
+ console.error(`[ZettelkastenEvolution] Error enriching related note:`, error);
228
+ }
229
+ }
230
+ const updatedMetadata = {
231
+ zettelkasten_keywords: extractedKeywords,
232
+ zettelkasten_tags: addedTags,
233
+ zettelkasten_related: relatedNotes.map(n => n.observationId),
234
+ zettelkasten_enriched: true,
235
+ zettelkasten_timestamp: Date.now()
236
+ };
237
+ // Fetch current metadata and merge with new metadata
238
+ const currentMetaResult = await this.db.run(`
239
+ ?[metadata] := *observation{id, metadata, @ "NOW"}, id = $id
240
+ `, { id: observationId });
241
+ const existingMetadata = currentMetaResult.rows[0]?.[0] || {};
242
+ const mergedMetadata = {
243
+ ...existingMetadata,
244
+ zettelkasten_keywords: extractedKeywords,
245
+ zettelkasten_tags: addedTags,
246
+ zettelkasten_related: relatedNotes.map(n => n.observationId),
247
+ zettelkasten_enriched: true,
248
+ zettelkasten_timestamp: Date.now()
249
+ };
250
+ await this.db.run(`
251
+ ?[id, created_at, entity_id, session_id, task_id, text, embedding, metadata] :=
252
+ *observation{id, created_at, entity_id, session_id, task_id, text, embedding, @ "NOW"},
253
+ id = $id,
254
+ metadata = $new_metadata
255
+
256
+ :put observation {id, created_at => entity_id, session_id, task_id, text, embedding, metadata}
257
+ `, {
258
+ id: observationId,
259
+ new_metadata: mergedMetadata
260
+ });
261
+ const evolutionSummary = `Enriched with ${extractedKeywords.length} keywords, ${addedTags.length} tags, ` +
262
+ `${relatedNotes.length} related notes, ${createdLinks} bidirectional links`;
263
+ return {
264
+ observationId,
265
+ relatedNotes,
266
+ extractedKeywords,
267
+ addedTags,
268
+ createdLinks,
269
+ updatedMetadata,
270
+ evolutionSummary
271
+ };
272
+ }
273
+ catch (error) {
274
+ console.error('[ZettelkastenEvolution] Error enriching observation:', error);
275
+ return {
276
+ observationId,
277
+ relatedNotes: [],
278
+ extractedKeywords: [],
279
+ addedTags: [],
280
+ createdLinks: 0,
281
+ updatedMetadata: {},
282
+ evolutionSummary: 'Enrichment failed'
283
+ };
284
+ }
285
+ }
286
+ async getEvolutionStats() {
287
+ try {
288
+ const totalResult = await this.db.run(`
289
+ ?[count(id)] := *observation{id, @ "NOW"}
290
+ `);
291
+ const totalObservations = totalResult.rows[0]?.[0] || 0;
292
+ const enrichedResult = await this.db.run(`
293
+ ?[count(id)] :=
294
+ *observation{id, metadata, @ "NOW"},
295
+ metadata != null,
296
+ metadata->'zettelkasten_enriched' == true
297
+ `);
298
+ const enrichedObservations = enrichedResult.rows[0]?.[0] || 0;
299
+ const linksResult = await this.db.run(`
300
+ ?[count(from_id)] :=
301
+ *relationship{from_id, to_id, relation_type, @ "NOW"},
302
+ relation_type == 'zettelkasten_link'
303
+ `);
304
+ const totalLinks = linksResult.rows[0]?.[0] || 0;
305
+ const averageLinksPerNote = enrichedObservations > 0
306
+ ? totalLinks / enrichedObservations
307
+ : 0;
308
+ return {
309
+ totalObservations,
310
+ enrichedObservations,
311
+ totalLinks,
312
+ averageLinksPerNote,
313
+ topKeywords: [],
314
+ topTags: [],
315
+ connectionTypes: {
316
+ semantic: 0,
317
+ keyword: 0,
318
+ entity: 0,
319
+ temporal: 0
320
+ }
321
+ };
322
+ }
323
+ catch (error) {
324
+ console.error('[ZettelkastenEvolution] Error getting stats:', error);
325
+ return {
326
+ totalObservations: 0,
327
+ enrichedObservations: 0,
328
+ totalLinks: 0,
329
+ averageLinksPerNote: 0,
330
+ topKeywords: [],
331
+ topTags: [],
332
+ connectionTypes: {
333
+ semantic: 0,
334
+ keyword: 0,
335
+ entity: 0,
336
+ temporal: 0
337
+ }
338
+ };
339
+ }
340
+ }
341
+ }
342
+ exports.ZettelkastenEvolutionService = ZettelkastenEvolutionService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cozo-memory",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "mcpName": "io.github.tobs-code/cozo-memory",
5
5
  "description": "Local-first persistent memory system for AI agents with hybrid search, graph reasoning, and MCP integration",
6
6
  "main": "dist/index.js",