cozo-memory 1.1.4 → 1.1.6
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 +161 -1159
- package/dist/adaptive-query-fusion.js +397 -0
- package/dist/dynamic-fusion.js +63 -8
- package/dist/explainable-retrieval.js +552 -0
- package/dist/hierarchical-memory.js +358 -0
- package/dist/proactive-suggestions.js +382 -0
- package/dist/temporal-conflict-resolution.js +386 -0
- package/dist/temporal-pattern-detection-backup.js +358 -0
- package/dist/temporal-pattern-detection.js +482 -0
- package/dist/test-adaptive-query-fusion.js +208 -0
- package/dist/test-explainable-retrieval.js +408 -0
- package/dist/test-hierarchical-and-patterns.js +17 -0
- package/dist/test-hierarchical-memory.js +205 -0
- package/dist/test-proactive-suggestions.js +240 -0
- package/dist/test-temporal-conflict-resolution.js +228 -0
- package/dist/test-temporal-patterns.js +317 -0
- package/package.json +1 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TemporalPatternDetectionService = exports.PatternType = void 0;
|
|
4
|
+
const uuid_1 = require("uuid");
|
|
5
|
+
var PatternType;
|
|
6
|
+
(function (PatternType) {
|
|
7
|
+
PatternType["RECURRING_EVENT"] = "recurring_event";
|
|
8
|
+
PatternType["CYCLICAL_RELATIONSHIP"] = "cyclical_relationship";
|
|
9
|
+
PatternType["TEMPORAL_CORRELATION"] = "temporal_correlation";
|
|
10
|
+
PatternType["SEASONAL_TREND"] = "seasonal_trend";
|
|
11
|
+
PatternType["ANOMALY"] = "anomaly";
|
|
12
|
+
})(PatternType || (exports.PatternType = PatternType = {}));
|
|
13
|
+
class TemporalPatternDetectionService {
|
|
14
|
+
db;
|
|
15
|
+
embeddings;
|
|
16
|
+
config;
|
|
17
|
+
constructor(db, embeddings, config = {}) {
|
|
18
|
+
this.db = db;
|
|
19
|
+
this.embeddings = embeddings;
|
|
20
|
+
this.config = {
|
|
21
|
+
min_occurrences: config.min_occurrences ?? 3,
|
|
22
|
+
min_confidence: config.min_confidence ?? 0.6,
|
|
23
|
+
time_window_days: config.time_window_days ?? 365,
|
|
24
|
+
similarity_threshold: config.similarity_threshold ?? 0.75,
|
|
25
|
+
seasonal_buckets: config.seasonal_buckets ?? 12
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
async detectPatterns(entityId) {
|
|
29
|
+
const patterns = [];
|
|
30
|
+
// Get entity name (without time travel since test doesn't use Validity)
|
|
31
|
+
let entityName = 'Unknown';
|
|
32
|
+
try {
|
|
33
|
+
const entityResult = await this.db.run(`
|
|
34
|
+
?[name] := *entity{id, name, @ "NOW"}, id = $entity_id
|
|
35
|
+
`, { entity_id: entityId });
|
|
36
|
+
entityName = entityResult.rows.length > 0 ? entityResult.rows[0][0] : 'Unknown';
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
// Fallback if Validity is not used
|
|
40
|
+
const entityResult = await this.db.run(`
|
|
41
|
+
?[name] := *entity{id, name}, id = $entity_id
|
|
42
|
+
`, { entity_id: entityId });
|
|
43
|
+
entityName = entityResult.rows.length > 0 ? entityResult.rows[0][0] : 'Unknown';
|
|
44
|
+
}
|
|
45
|
+
// 1. Detect recurring events
|
|
46
|
+
const recurringPatterns = await this.detectRecurringEvents(entityId, entityName);
|
|
47
|
+
patterns.push(...recurringPatterns);
|
|
48
|
+
// 2. Detect cyclical relationships
|
|
49
|
+
const cyclicalPatterns = await this.detectCyclicalRelationships(entityId, entityName);
|
|
50
|
+
patterns.push(...cyclicalPatterns);
|
|
51
|
+
// 3. Detect temporal correlations
|
|
52
|
+
const correlationPatterns = await this.detectTemporalCorrelations(entityId, entityName);
|
|
53
|
+
patterns.push(...correlationPatterns);
|
|
54
|
+
// 4. Detect seasonal trends
|
|
55
|
+
const seasonalPatterns = await this.detectSeasonalTrends(entityId, entityName);
|
|
56
|
+
patterns.push(...seasonalPatterns);
|
|
57
|
+
return patterns;
|
|
58
|
+
}
|
|
59
|
+
async detectRecurringEvents(entityId, entityName) {
|
|
60
|
+
const patterns = [];
|
|
61
|
+
const timeWindowMicros = this.config.time_window_days * 24 * 60 * 60 * 1000 * 1000;
|
|
62
|
+
const nowMicros = Date.now() * 1000;
|
|
63
|
+
const startTime = nowMicros - timeWindowMicros;
|
|
64
|
+
// Get all observations for this entity within time window
|
|
65
|
+
const obsResult = await this.db.run(`
|
|
66
|
+
?[id, text, embedding, created_at] :=
|
|
67
|
+
*observation{id, entity_id, text, embedding, created_at},
|
|
68
|
+
entity_id = $entity_id,
|
|
69
|
+
to_int(created_at) >= $start_time
|
|
70
|
+
`, { entity_id: entityId, start_time: startTime });
|
|
71
|
+
if (obsResult.rows.length < this.config.min_occurrences) {
|
|
72
|
+
return patterns;
|
|
73
|
+
}
|
|
74
|
+
// Group similar observations using embeddings
|
|
75
|
+
const observations = obsResult.rows.map((row) => {
|
|
76
|
+
const createdAt = row[3];
|
|
77
|
+
const timestamp = Array.isArray(createdAt) ? createdAt[0] : createdAt;
|
|
78
|
+
return {
|
|
79
|
+
id: row[0],
|
|
80
|
+
text: row[1],
|
|
81
|
+
embedding: row[2],
|
|
82
|
+
timestamp
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
// Cluster similar observations
|
|
86
|
+
const clusters = this.clusterObservations(observations);
|
|
87
|
+
// Analyze each cluster for recurring patterns
|
|
88
|
+
for (const cluster of clusters) {
|
|
89
|
+
if (cluster.length < this.config.min_occurrences)
|
|
90
|
+
continue;
|
|
91
|
+
// Sort by timestamp
|
|
92
|
+
cluster.sort((a, b) => a.timestamp - b.timestamp);
|
|
93
|
+
// Calculate intervals between occurrences
|
|
94
|
+
const intervals = [];
|
|
95
|
+
for (let i = 1; i < cluster.length; i++) {
|
|
96
|
+
const intervalMs = (cluster[i].timestamp - cluster[i - 1].timestamp) / 1000;
|
|
97
|
+
const intervalDays = intervalMs / (24 * 60 * 60 * 1000);
|
|
98
|
+
intervals.push(intervalDays);
|
|
99
|
+
}
|
|
100
|
+
// Check if intervals are relatively consistent (recurring pattern)
|
|
101
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
102
|
+
const variance = intervals.reduce((sum, interval) => sum + Math.pow(interval - avgInterval, 2), 0) / intervals.length;
|
|
103
|
+
const stdDev = Math.sqrt(variance);
|
|
104
|
+
const coefficientOfVariation = stdDev / avgInterval;
|
|
105
|
+
// If coefficient of variation is low, it's a recurring pattern
|
|
106
|
+
if (coefficientOfVariation < 0.3) {
|
|
107
|
+
const confidence = Math.max(0, 1 - coefficientOfVariation);
|
|
108
|
+
if (confidence >= this.config.min_confidence) {
|
|
109
|
+
patterns.push({
|
|
110
|
+
id: (0, uuid_1.v4)(),
|
|
111
|
+
pattern_type: PatternType.RECURRING_EVENT,
|
|
112
|
+
entity_id: entityId,
|
|
113
|
+
entity_name: entityName,
|
|
114
|
+
description: `Recurring event: "${cluster[0].text}" occurs approximately every ${avgInterval.toFixed(0)} days`,
|
|
115
|
+
frequency: cluster.length,
|
|
116
|
+
confidence,
|
|
117
|
+
first_occurrence: cluster[0].timestamp,
|
|
118
|
+
last_occurrence: cluster[cluster.length - 1].timestamp,
|
|
119
|
+
interval_days: avgInterval,
|
|
120
|
+
occurrences: cluster.map(obs => ({
|
|
121
|
+
timestamp: obs.timestamp,
|
|
122
|
+
observation_id: obs.id,
|
|
123
|
+
text: obs.text
|
|
124
|
+
})),
|
|
125
|
+
metadata: {
|
|
126
|
+
avg_interval_days: avgInterval,
|
|
127
|
+
std_dev_days: stdDev,
|
|
128
|
+
coefficient_of_variation: coefficientOfVariation
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return patterns;
|
|
135
|
+
}
|
|
136
|
+
async detectCyclicalRelationships(entityId, entityName) {
|
|
137
|
+
const patterns = [];
|
|
138
|
+
// Find cycles in the relationship graph starting from this entity
|
|
139
|
+
// A cycle is: A -> B -> ... -> A
|
|
140
|
+
const cycleResult = await this.db.run(`
|
|
141
|
+
cycle[from_id, to_id, path] :=
|
|
142
|
+
*relationship{from_id, to_id},
|
|
143
|
+
from_id = $entity_id,
|
|
144
|
+
path = [from_id, to_id]
|
|
145
|
+
|
|
146
|
+
cycle[from_id, to_id, path] :=
|
|
147
|
+
cycle[from_id, mid, prev_path],
|
|
148
|
+
*relationship{from_id: mid, to_id},
|
|
149
|
+
to_id != from_id,
|
|
150
|
+
!is_in(to_id, prev_path),
|
|
151
|
+
length(prev_path) < 10,
|
|
152
|
+
path = append(prev_path, to_id)
|
|
153
|
+
|
|
154
|
+
?[from_id, to_id, path] :=
|
|
155
|
+
cycle[from_id, to_id, path],
|
|
156
|
+
to_id = $entity_id,
|
|
157
|
+
length(path) >= 3
|
|
158
|
+
`, { entity_id: entityId });
|
|
159
|
+
for (const row of cycleResult.rows) {
|
|
160
|
+
const path = row[2];
|
|
161
|
+
const cycleLength = path.length;
|
|
162
|
+
// Get relationship types in the cycle
|
|
163
|
+
const relationTypes = [];
|
|
164
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
165
|
+
const relResult = await this.db.run(`
|
|
166
|
+
?[relation_type] :=
|
|
167
|
+
*relationship{from_id, to_id, relation_type},
|
|
168
|
+
from_id = $from_id,
|
|
169
|
+
to_id = $to_id
|
|
170
|
+
`, { from_id: path[i], to_id: path[i + 1] });
|
|
171
|
+
if (relResult.rows.length > 0) {
|
|
172
|
+
relationTypes.push(relResult.rows[0][0]);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Add closing relationship
|
|
176
|
+
const closingRelResult = await this.db.run(`
|
|
177
|
+
?[relation_type] :=
|
|
178
|
+
*relationship{from_id, to_id, relation_type},
|
|
179
|
+
from_id = $from_id,
|
|
180
|
+
to_id = $to_id
|
|
181
|
+
`, { from_id: path[path.length - 1], to_id: path[0] });
|
|
182
|
+
if (closingRelResult.rows.length > 0) {
|
|
183
|
+
relationTypes.push(closingRelResult.rows[0][0]);
|
|
184
|
+
}
|
|
185
|
+
const confidence = Math.min(1.0, 0.7 + (0.1 * cycleLength));
|
|
186
|
+
if (confidence >= this.config.min_confidence) {
|
|
187
|
+
patterns.push({
|
|
188
|
+
id: (0, uuid_1.v4)(),
|
|
189
|
+
pattern_type: PatternType.CYCLICAL_RELATIONSHIP,
|
|
190
|
+
entity_id: entityId,
|
|
191
|
+
entity_name: entityName,
|
|
192
|
+
description: `Cyclical relationship detected: ${relationTypes.join(' → ')}`,
|
|
193
|
+
frequency: 1,
|
|
194
|
+
confidence,
|
|
195
|
+
first_occurrence: Date.now() * 1000,
|
|
196
|
+
last_occurrence: Date.now() * 1000,
|
|
197
|
+
occurrences: [],
|
|
198
|
+
metadata: {
|
|
199
|
+
cycle_path: path,
|
|
200
|
+
cycle_length: cycleLength,
|
|
201
|
+
relation_types: relationTypes
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return patterns;
|
|
207
|
+
}
|
|
208
|
+
async detectTemporalCorrelations(entityId, entityName) {
|
|
209
|
+
const patterns = [];
|
|
210
|
+
const timeWindowMicros = this.config.time_window_days * 24 * 60 * 60 * 1000 * 1000;
|
|
211
|
+
const nowMicros = Date.now() * 1000;
|
|
212
|
+
const startTime = nowMicros - timeWindowMicros;
|
|
213
|
+
// Get all observations
|
|
214
|
+
const obsResult = await this.db.run(`
|
|
215
|
+
?[id, text, embedding, created_at] :=
|
|
216
|
+
*observation{id, entity_id, text, embedding, created_at},
|
|
217
|
+
entity_id = $entity_id,
|
|
218
|
+
to_int(created_at) >= $start_time
|
|
219
|
+
`, { entity_id: entityId, start_time: startTime });
|
|
220
|
+
if (obsResult.rows.length < this.config.min_occurrences * 2) {
|
|
221
|
+
return patterns;
|
|
222
|
+
}
|
|
223
|
+
const observations = obsResult.rows.map((row) => {
|
|
224
|
+
const createdAt = row[3];
|
|
225
|
+
const timestamp = Array.isArray(createdAt) ? createdAt[0] : createdAt;
|
|
226
|
+
return {
|
|
227
|
+
id: row[0],
|
|
228
|
+
text: row[1],
|
|
229
|
+
embedding: row[2],
|
|
230
|
+
timestamp
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
// Find pairs of different observation types that occur close together
|
|
234
|
+
const correlationWindow = 2 * 24 * 60 * 60 * 1000 * 1000; // 2 days in microseconds
|
|
235
|
+
const correlations = new Map();
|
|
236
|
+
for (let i = 0; i < observations.length; i++) {
|
|
237
|
+
for (let j = i + 1; j < observations.length; j++) {
|
|
238
|
+
const timeDiff = Math.abs(observations[j].timestamp - observations[i].timestamp);
|
|
239
|
+
if (timeDiff <= correlationWindow) {
|
|
240
|
+
const similarity = this.cosineSimilarity(observations[i].embedding, observations[j].embedding);
|
|
241
|
+
// Only correlate different types of events (low similarity)
|
|
242
|
+
if (similarity < 0.5) {
|
|
243
|
+
const key = [observations[i].text, observations[j].text].sort().join('|||');
|
|
244
|
+
if (!correlations.has(key)) {
|
|
245
|
+
correlations.set(key, []);
|
|
246
|
+
}
|
|
247
|
+
correlations.get(key).push({
|
|
248
|
+
obs1: observations[i],
|
|
249
|
+
obs2: observations[j]
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Analyze correlations
|
|
256
|
+
for (const [key, pairs] of correlations.entries()) {
|
|
257
|
+
if (pairs.length >= this.config.min_occurrences) {
|
|
258
|
+
const [text1, text2] = key.split('|||');
|
|
259
|
+
const confidence = Math.min(1.0, pairs.length / (this.config.min_occurrences * 2));
|
|
260
|
+
if (confidence >= this.config.min_confidence) {
|
|
261
|
+
patterns.push({
|
|
262
|
+
id: (0, uuid_1.v4)(),
|
|
263
|
+
pattern_type: PatternType.TEMPORAL_CORRELATION,
|
|
264
|
+
entity_id: entityId,
|
|
265
|
+
entity_name: entityName,
|
|
266
|
+
description: `Temporal correlation: "${text1}" often occurs near "${text2}"`,
|
|
267
|
+
frequency: pairs.length,
|
|
268
|
+
confidence,
|
|
269
|
+
first_occurrence: Math.min(...pairs.map(p => Math.min(p.obs1.timestamp, p.obs2.timestamp))),
|
|
270
|
+
last_occurrence: Math.max(...pairs.map(p => Math.max(p.obs1.timestamp, p.obs2.timestamp))),
|
|
271
|
+
occurrences: pairs.flatMap(p => [
|
|
272
|
+
{ timestamp: p.obs1.timestamp, observation_id: p.obs1.id, text: p.obs1.text },
|
|
273
|
+
{ timestamp: p.obs2.timestamp, observation_id: p.obs2.id, text: p.obs2.text }
|
|
274
|
+
]),
|
|
275
|
+
metadata: {
|
|
276
|
+
event_pair: [text1, text2],
|
|
277
|
+
correlation_window_days: correlationWindow / (24 * 60 * 60 * 1000 * 1000)
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return patterns;
|
|
284
|
+
}
|
|
285
|
+
async detectSeasonalTrends(entityId, entityName) {
|
|
286
|
+
const patterns = [];
|
|
287
|
+
const timeWindowMicros = this.config.time_window_days * 24 * 60 * 60 * 1000 * 1000;
|
|
288
|
+
const nowMicros = Date.now() * 1000;
|
|
289
|
+
const startTime = nowMicros - timeWindowMicros;
|
|
290
|
+
// Get all observations
|
|
291
|
+
const obsResult = await this.db.run(`
|
|
292
|
+
?[id, text, created_at] :=
|
|
293
|
+
*observation{id, entity_id, text, created_at},
|
|
294
|
+
entity_id = $entity_id,
|
|
295
|
+
to_int(created_at) >= $start_time
|
|
296
|
+
`, { entity_id: entityId, start_time: startTime });
|
|
297
|
+
if (obsResult.rows.length < this.config.min_occurrences) {
|
|
298
|
+
return patterns;
|
|
299
|
+
}
|
|
300
|
+
// Group observations by seasonal bucket (month or quarter)
|
|
301
|
+
const buckets = new Map();
|
|
302
|
+
for (const row of obsResult.rows) {
|
|
303
|
+
const createdAt = row[2];
|
|
304
|
+
// Handle both Validity tuple [timestamp, valid] and plain timestamp
|
|
305
|
+
const timestamp = Array.isArray(createdAt) ? createdAt[0] : createdAt;
|
|
306
|
+
const date = new Date(timestamp / 1000); // Convert microseconds to milliseconds
|
|
307
|
+
const bucket = date.getMonth(); // 0-11 for monthly buckets
|
|
308
|
+
if (!buckets.has(bucket)) {
|
|
309
|
+
buckets.set(bucket, []);
|
|
310
|
+
}
|
|
311
|
+
buckets.get(bucket).push({
|
|
312
|
+
id: row[0],
|
|
313
|
+
text: row[1],
|
|
314
|
+
timestamp
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
// Calculate average activity per bucket
|
|
318
|
+
const avgActivity = obsResult.rows.length / this.config.seasonal_buckets;
|
|
319
|
+
// Find buckets with significantly higher activity
|
|
320
|
+
for (const [bucket, observations] of buckets.entries()) {
|
|
321
|
+
const activityRatio = observations.length / avgActivity;
|
|
322
|
+
// If activity is 2x or more than average, it's a seasonal trend
|
|
323
|
+
if (activityRatio >= 2.0 && observations.length >= this.config.min_occurrences) {
|
|
324
|
+
const confidence = Math.min(1.0, activityRatio / 3);
|
|
325
|
+
if (confidence >= this.config.min_confidence) {
|
|
326
|
+
const monthName = new Date(2024, bucket, 1).toLocaleString('default', { month: 'long' });
|
|
327
|
+
patterns.push({
|
|
328
|
+
id: (0, uuid_1.v4)(),
|
|
329
|
+
pattern_type: PatternType.SEASONAL_TREND,
|
|
330
|
+
entity_id: entityId,
|
|
331
|
+
entity_name: entityName,
|
|
332
|
+
description: `Seasonal trend: Increased activity in ${monthName} (${activityRatio.toFixed(1)}x average)`,
|
|
333
|
+
frequency: observations.length,
|
|
334
|
+
confidence,
|
|
335
|
+
first_occurrence: Math.min(...observations.map(o => o.timestamp)),
|
|
336
|
+
last_occurrence: Math.max(...observations.map(o => o.timestamp)),
|
|
337
|
+
occurrences: observations.map(obs => ({
|
|
338
|
+
timestamp: obs.timestamp,
|
|
339
|
+
observation_id: obs.id,
|
|
340
|
+
text: obs.text
|
|
341
|
+
})),
|
|
342
|
+
metadata: {
|
|
343
|
+
seasonal_bucket: bucket,
|
|
344
|
+
bucket_name: monthName,
|
|
345
|
+
activity_ratio: activityRatio,
|
|
346
|
+
avg_activity: avgActivity
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return patterns;
|
|
353
|
+
}
|
|
354
|
+
clusterObservations(observations) {
|
|
355
|
+
const clusters = [];
|
|
356
|
+
const used = new Set();
|
|
357
|
+
for (let i = 0; i < observations.length; i++) {
|
|
358
|
+
if (used.has(i))
|
|
359
|
+
continue;
|
|
360
|
+
const cluster = [observations[i]];
|
|
361
|
+
used.add(i);
|
|
362
|
+
for (let j = i + 1; j < observations.length; j++) {
|
|
363
|
+
if (used.has(j))
|
|
364
|
+
continue;
|
|
365
|
+
const similarity = this.cosineSimilarity(observations[i].embedding, observations[j].embedding);
|
|
366
|
+
if (similarity >= this.config.similarity_threshold) {
|
|
367
|
+
cluster.push(observations[j]);
|
|
368
|
+
used.add(j);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
clusters.push(cluster);
|
|
372
|
+
}
|
|
373
|
+
return clusters;
|
|
374
|
+
}
|
|
375
|
+
cosineSimilarity(a, b) {
|
|
376
|
+
if (a.length !== b.length)
|
|
377
|
+
return 0;
|
|
378
|
+
let dotProduct = 0;
|
|
379
|
+
let normA = 0;
|
|
380
|
+
let normB = 0;
|
|
381
|
+
for (let i = 0; i < a.length; i++) {
|
|
382
|
+
dotProduct += a[i] * b[i];
|
|
383
|
+
normA += a[i] * a[i];
|
|
384
|
+
normB += b[i] * b[i];
|
|
385
|
+
}
|
|
386
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
387
|
+
return denominator === 0 ? 0 : dotProduct / denominator;
|
|
388
|
+
}
|
|
389
|
+
async storePattern(pattern) {
|
|
390
|
+
// Create temporal_pattern relation if it doesn't exist (only once)
|
|
391
|
+
try {
|
|
392
|
+
await this.db.run(`
|
|
393
|
+
:create temporal_pattern {
|
|
394
|
+
id: String =>
|
|
395
|
+
pattern_type: String,
|
|
396
|
+
entity_id: String,
|
|
397
|
+
entity_name: String,
|
|
398
|
+
description: String,
|
|
399
|
+
frequency: Int,
|
|
400
|
+
confidence: Float,
|
|
401
|
+
first_occurrence: Int,
|
|
402
|
+
last_occurrence: Int,
|
|
403
|
+
interval_days: Float?,
|
|
404
|
+
metadata: Json
|
|
405
|
+
}
|
|
406
|
+
`);
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
// Relation already exists, that's fine - continue with insert
|
|
410
|
+
if (!error.display?.includes('already exists') && !error.display?.includes('conflicts with an existing')) {
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
await this.db.run(`
|
|
415
|
+
?[id, pattern_type, entity_id, entity_name, description, frequency, confidence, first_occurrence, last_occurrence, interval_days, metadata] :=
|
|
416
|
+
id = $id,
|
|
417
|
+
pattern_type = $pattern_type,
|
|
418
|
+
entity_id = $entity_id,
|
|
419
|
+
entity_name = $entity_name,
|
|
420
|
+
description = $description,
|
|
421
|
+
frequency = $frequency,
|
|
422
|
+
confidence = $confidence,
|
|
423
|
+
first_occurrence = $first_occurrence,
|
|
424
|
+
last_occurrence = $last_occurrence,
|
|
425
|
+
interval_days = $interval_days,
|
|
426
|
+
metadata = $metadata
|
|
427
|
+
|
|
428
|
+
:put temporal_pattern {
|
|
429
|
+
id => pattern_type, entity_id, entity_name, description, frequency, confidence, first_occurrence, last_occurrence, interval_days, metadata
|
|
430
|
+
}
|
|
431
|
+
`, {
|
|
432
|
+
id: pattern.id,
|
|
433
|
+
pattern_type: pattern.pattern_type,
|
|
434
|
+
entity_id: pattern.entity_id,
|
|
435
|
+
entity_name: pattern.entity_name,
|
|
436
|
+
description: pattern.description,
|
|
437
|
+
frequency: pattern.frequency,
|
|
438
|
+
confidence: pattern.confidence,
|
|
439
|
+
first_occurrence: pattern.first_occurrence,
|
|
440
|
+
last_occurrence: pattern.last_occurrence,
|
|
441
|
+
interval_days: pattern.interval_days ?? null,
|
|
442
|
+
metadata: pattern.metadata ?? {}
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
async getStoredPatterns(entityId, patternType) {
|
|
446
|
+
let query = `
|
|
447
|
+
?[id, pattern_type, entity_id, entity_name, description, frequency, confidence, first_occurrence, last_occurrence, interval_days, metadata] :=
|
|
448
|
+
*temporal_pattern{id, pattern_type, entity_id, entity_name, description, frequency, confidence, first_occurrence, last_occurrence, interval_days, metadata}
|
|
449
|
+
`;
|
|
450
|
+
const params = {};
|
|
451
|
+
if (entityId) {
|
|
452
|
+
query += `, entity_id = $entity_id`;
|
|
453
|
+
params.entity_id = entityId;
|
|
454
|
+
}
|
|
455
|
+
if (patternType) {
|
|
456
|
+
query += `, pattern_type = $pattern_type`;
|
|
457
|
+
params.pattern_type = patternType;
|
|
458
|
+
}
|
|
459
|
+
const result = await this.db.run(query, params);
|
|
460
|
+
return result.rows.map((row) => ({
|
|
461
|
+
id: row[0],
|
|
462
|
+
pattern_type: row[1],
|
|
463
|
+
entity_id: row[2],
|
|
464
|
+
entity_name: row[3],
|
|
465
|
+
description: row[4],
|
|
466
|
+
frequency: row[5],
|
|
467
|
+
confidence: row[6],
|
|
468
|
+
first_occurrence: row[7],
|
|
469
|
+
last_occurrence: row[8],
|
|
470
|
+
interval_days: row[9],
|
|
471
|
+
occurrences: [],
|
|
472
|
+
metadata: row[10]
|
|
473
|
+
}));
|
|
474
|
+
}
|
|
475
|
+
updateConfig(config) {
|
|
476
|
+
this.config = { ...this.config, ...config };
|
|
477
|
+
}
|
|
478
|
+
getConfig() {
|
|
479
|
+
return { ...this.config };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
exports.TemporalPatternDetectionService = TemporalPatternDetectionService;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test: Adaptive Query Fusion with Dynamic Weights
|
|
4
|
+
*
|
|
5
|
+
* Tests query classification and adaptive weight selection
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
const cozo_node_1 = require("cozo-node");
|
|
42
|
+
const embedding_service_1 = require("./embedding-service");
|
|
43
|
+
const adaptive_query_fusion_1 = require("./adaptive-query-fusion");
|
|
44
|
+
const fs = __importStar(require("fs"));
|
|
45
|
+
const DB_PATH = 'test-adaptive-fusion.db';
|
|
46
|
+
async function testAdaptiveQueryFusion() {
|
|
47
|
+
// Clean up old database
|
|
48
|
+
if (fs.existsSync(DB_PATH)) {
|
|
49
|
+
try {
|
|
50
|
+
fs.unlinkSync(DB_PATH);
|
|
51
|
+
}
|
|
52
|
+
catch (e) { }
|
|
53
|
+
}
|
|
54
|
+
const db = new cozo_node_1.CozoDb('sqlite', DB_PATH);
|
|
55
|
+
const embeddingService = new embedding_service_1.EmbeddingService();
|
|
56
|
+
try {
|
|
57
|
+
console.log('=== Adaptive Query Fusion Tests ===\n');
|
|
58
|
+
const fusion = new adaptive_query_fusion_1.AdaptiveQueryFusion(db, embeddingService, {
|
|
59
|
+
enableLLM: false, // Disable LLM for testing (no Ollama required)
|
|
60
|
+
cacheClassifications: true,
|
|
61
|
+
maxCacheSize: 100
|
|
62
|
+
});
|
|
63
|
+
// Test cases with expected classifications
|
|
64
|
+
const testQueries = [
|
|
65
|
+
// FINDER queries (factual information seeking)
|
|
66
|
+
{
|
|
67
|
+
query: 'What is the capital of France?',
|
|
68
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.FINDER,
|
|
69
|
+
description: 'Simple factual question'
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
query: 'When was the first iPhone released?',
|
|
73
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.FINDER,
|
|
74
|
+
description: 'Factual query with temporal aspect'
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
query: 'Find me the latest news about AI developments',
|
|
78
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.FINDER,
|
|
79
|
+
description: 'Information seeking with recency'
|
|
80
|
+
},
|
|
81
|
+
// EVALUATOR queries (comparison/evaluation)
|
|
82
|
+
{
|
|
83
|
+
query: 'Compare TypeScript vs JavaScript',
|
|
84
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.EVALUATOR,
|
|
85
|
+
description: 'Direct comparison'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
query: 'What are the pros and cons of microservices?',
|
|
89
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.EVALUATOR,
|
|
90
|
+
description: 'Evaluation query'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
query: 'Which database is better for my use case?',
|
|
94
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.EVALUATOR,
|
|
95
|
+
description: 'Comparative evaluation'
|
|
96
|
+
},
|
|
97
|
+
// EXPLAINER queries (concept understanding)
|
|
98
|
+
{
|
|
99
|
+
query: 'How does machine learning work?',
|
|
100
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.EXPLAINER,
|
|
101
|
+
description: 'Concept explanation'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
query: 'Explain the concept of blockchain',
|
|
105
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.EXPLAINER,
|
|
106
|
+
description: 'Direct explanation request'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
query: 'Why is caching important in web development?',
|
|
110
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.EXPLAINER,
|
|
111
|
+
description: 'Why question (explanation)'
|
|
112
|
+
},
|
|
113
|
+
// GENERATOR queries (content creation)
|
|
114
|
+
{
|
|
115
|
+
query: 'Write a Python function to calculate fibonacci',
|
|
116
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.GENERATOR,
|
|
117
|
+
description: 'Code generation'
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
query: 'Create a marketing strategy for a startup',
|
|
121
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.GENERATOR,
|
|
122
|
+
description: 'Content generation'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
query: 'Suggest a project structure for a Node.js app',
|
|
126
|
+
expectedIntent: adaptive_query_fusion_1.QueryIntent.GENERATOR,
|
|
127
|
+
description: 'Suggestion/recommendation'
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
console.log('--- Test 1: Query Classification ---\n');
|
|
131
|
+
for (const testCase of testQueries) {
|
|
132
|
+
const classification = await fusion.classifyQuery(testCase.query);
|
|
133
|
+
const match = classification.intent === testCase.expectedIntent ? '✓' : '✗';
|
|
134
|
+
console.log(`${match} "${testCase.query}"`);
|
|
135
|
+
console.log(` Description: ${testCase.description}`);
|
|
136
|
+
console.log(` Intent: ${classification.intent} (expected: ${testCase.expectedIntent})`);
|
|
137
|
+
console.log(` Complexity: ${classification.complexity}`);
|
|
138
|
+
console.log(` Confidence: ${(classification.confidence * 100).toFixed(0)}%`);
|
|
139
|
+
console.log(` Method: ${classification.method}`);
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
console.log('--- Test 2: Adaptive Weights ---\n');
|
|
143
|
+
const weightTestQueries = [
|
|
144
|
+
'What is TypeScript?', // FINDER + SIMPLE
|
|
145
|
+
'Compare React vs Vue', // EVALUATOR + MODERATE
|
|
146
|
+
'Explain how async/await works', // EXPLAINER + MODERATE
|
|
147
|
+
'Create a REST API' // GENERATOR + MODERATE
|
|
148
|
+
];
|
|
149
|
+
for (const query of weightTestQueries) {
|
|
150
|
+
const weights = await fusion.getAdaptiveWeights(query);
|
|
151
|
+
const classification = await fusion.getClassificationDetails(query);
|
|
152
|
+
console.log(`Query: "${query}"`);
|
|
153
|
+
console.log(`Intent: ${classification.intent}, Complexity: ${classification.complexity}`);
|
|
154
|
+
console.log(`Weights:`);
|
|
155
|
+
console.log(` Vector: ${(weights.vector * 100).toFixed(0)}%`);
|
|
156
|
+
console.log(` Sparse: ${(weights.sparse * 100).toFixed(0)}%`);
|
|
157
|
+
console.log(` FTS: ${(weights.fts * 100).toFixed(0)}%`);
|
|
158
|
+
console.log(` Graph: ${(weights.graph * 100).toFixed(0)}%`);
|
|
159
|
+
console.log();
|
|
160
|
+
}
|
|
161
|
+
console.log('--- Test 3: Caching ---\n');
|
|
162
|
+
const cacheTestQuery = 'What is the meaning of life?';
|
|
163
|
+
// First call (not cached)
|
|
164
|
+
console.log(`First call: "${cacheTestQuery}"`);
|
|
165
|
+
const start1 = Date.now();
|
|
166
|
+
const result1 = await fusion.classifyQuery(cacheTestQuery);
|
|
167
|
+
const time1 = Date.now() - start1;
|
|
168
|
+
console.log(`Time: ${time1}ms`);
|
|
169
|
+
// Second call (cached)
|
|
170
|
+
console.log(`\nSecond call (should be cached): "${cacheTestQuery}"`);
|
|
171
|
+
const start2 = Date.now();
|
|
172
|
+
const result2 = await fusion.classifyQuery(cacheTestQuery);
|
|
173
|
+
const time2 = Date.now() - start2;
|
|
174
|
+
console.log(`Time: ${time2}ms`);
|
|
175
|
+
console.log(`Speedup: ${(time1 / time2).toFixed(1)}x faster`);
|
|
176
|
+
const cacheStats = fusion.getCacheStats();
|
|
177
|
+
console.log(`\nCache stats: ${cacheStats.size}/${cacheStats.maxSize} entries`);
|
|
178
|
+
console.log('\n--- Test 4: Complexity Classification ---\n');
|
|
179
|
+
const complexityTests = [
|
|
180
|
+
{ query: 'What is AI?', expected: adaptive_query_fusion_1.QueryComplexity.SIMPLE },
|
|
181
|
+
{ query: 'How does machine learning relate to AI?', expected: adaptive_query_fusion_1.QueryComplexity.MODERATE },
|
|
182
|
+
{ query: 'Explain the relationship between neural networks, deep learning, and AI, and how they impact modern software development', expected: adaptive_query_fusion_1.QueryComplexity.COMPLEX },
|
|
183
|
+
{ query: 'Tell me everything about cloud computing', expected: adaptive_query_fusion_1.QueryComplexity.EXPLORATORY }
|
|
184
|
+
];
|
|
185
|
+
for (const test of complexityTests) {
|
|
186
|
+
const classification = await fusion.classifyQuery(test.query);
|
|
187
|
+
const match = classification.complexity === test.expected ? '✓' : '✗';
|
|
188
|
+
console.log(`${match} "${test.query}"`);
|
|
189
|
+
console.log(` Complexity: ${classification.complexity} (expected: ${test.expected})`);
|
|
190
|
+
console.log();
|
|
191
|
+
}
|
|
192
|
+
console.log('=== All Tests Completed ===');
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('Test failed:', error);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await db.close();
|
|
199
|
+
// Clean up
|
|
200
|
+
if (fs.existsSync(DB_PATH)) {
|
|
201
|
+
try {
|
|
202
|
+
fs.unlinkSync(DB_PATH);
|
|
203
|
+
}
|
|
204
|
+
catch (e) { }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
testAdaptiveQueryFusion();
|