@soleri/core 2.0.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brain/brain.d.ts +12 -50
- package/dist/brain/brain.d.ts.map +1 -1
- package/dist/brain/brain.js +147 -12
- package/dist/brain/brain.js.map +1 -1
- package/dist/brain/intelligence.d.ts +51 -0
- package/dist/brain/intelligence.d.ts.map +1 -0
- package/dist/brain/intelligence.js +666 -0
- package/dist/brain/intelligence.js.map +1 -0
- package/dist/brain/types.d.ts +165 -0
- package/dist/brain/types.d.ts.map +1 -0
- package/dist/brain/types.js +2 -0
- package/dist/brain/types.js.map +1 -0
- package/dist/cognee/client.d.ts +35 -0
- package/dist/cognee/client.d.ts.map +1 -0
- package/dist/cognee/client.js +291 -0
- package/dist/cognee/client.js.map +1 -0
- package/dist/cognee/types.d.ts +46 -0
- package/dist/cognee/types.d.ts.map +1 -0
- package/dist/cognee/types.js +3 -0
- package/dist/cognee/types.js.map +1 -0
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +7 -5
- package/dist/curator/curator.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/llm-client.d.ts.map +1 -1
- package/dist/llm/llm-client.js +9 -2
- package/dist/llm/llm-client.js.map +1 -1
- package/dist/runtime/core-ops.d.ts +3 -3
- package/dist/runtime/core-ops.d.ts.map +1 -1
- package/dist/runtime/core-ops.js +180 -15
- package/dist/runtime/core-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +4 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/brain-intelligence.test.ts +623 -0
- package/src/__tests__/brain.test.ts +265 -27
- package/src/__tests__/cognee-client.test.ts +524 -0
- package/src/__tests__/core-ops.test.ts +77 -49
- package/src/__tests__/curator.test.ts +126 -31
- package/src/__tests__/domain-ops.test.ts +45 -9
- package/src/__tests__/runtime.test.ts +13 -11
- package/src/brain/brain.ts +194 -65
- package/src/brain/intelligence.ts +1061 -0
- package/src/brain/types.ts +176 -0
- package/src/cognee/client.ts +352 -0
- package/src/cognee/types.ts +62 -0
- package/src/curator/curator.ts +52 -15
- package/src/index.ts +26 -1
- package/src/llm/llm-client.ts +18 -24
- package/src/runtime/core-ops.ts +219 -26
- package/src/runtime/runtime.ts +5 -0
- package/src/runtime/types.ts +2 -0
package/src/brain/brain.ts
CHANGED
|
@@ -8,59 +8,27 @@ import {
|
|
|
8
8
|
cosineSimilarity,
|
|
9
9
|
jaccardSimilarity,
|
|
10
10
|
} from '../text/similarity.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
entry: IntelligenceEntry;
|
|
33
|
-
score: number;
|
|
34
|
-
breakdown: ScoreBreakdown;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface SearchOptions {
|
|
38
|
-
domain?: string;
|
|
39
|
-
type?: string;
|
|
40
|
-
severity?: string;
|
|
41
|
-
limit?: number;
|
|
42
|
-
tags?: string[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface CaptureResult {
|
|
46
|
-
captured: boolean;
|
|
47
|
-
id: string;
|
|
48
|
-
autoTags: string[];
|
|
49
|
-
duplicate?: { id: string; similarity: number };
|
|
50
|
-
blocked?: boolean;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface BrainStats {
|
|
54
|
-
vocabularySize: number;
|
|
55
|
-
feedbackCount: number;
|
|
56
|
-
weights: ScoringWeights;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface QueryContext {
|
|
60
|
-
query: string;
|
|
61
|
-
domain?: string;
|
|
62
|
-
tags?: string[];
|
|
63
|
-
}
|
|
11
|
+
import type { CogneeClient } from '../cognee/client.js';
|
|
12
|
+
import type {
|
|
13
|
+
ScoringWeights,
|
|
14
|
+
ScoreBreakdown,
|
|
15
|
+
RankedResult,
|
|
16
|
+
SearchOptions,
|
|
17
|
+
CaptureResult,
|
|
18
|
+
BrainStats,
|
|
19
|
+
QueryContext,
|
|
20
|
+
} from './types.js';
|
|
21
|
+
|
|
22
|
+
// Re-export types for backward compatibility
|
|
23
|
+
export type {
|
|
24
|
+
ScoringWeights,
|
|
25
|
+
ScoreBreakdown,
|
|
26
|
+
RankedResult,
|
|
27
|
+
SearchOptions,
|
|
28
|
+
CaptureResult,
|
|
29
|
+
BrainStats,
|
|
30
|
+
QueryContext,
|
|
31
|
+
} from './types.js';
|
|
64
32
|
|
|
65
33
|
// ─── Severity scoring ──────────────────────────────────────────────
|
|
66
34
|
|
|
@@ -74,12 +42,22 @@ const SEVERITY_SCORES: Record<string, number> = {
|
|
|
74
42
|
|
|
75
43
|
const DEFAULT_WEIGHTS: ScoringWeights = {
|
|
76
44
|
semantic: 0.4,
|
|
45
|
+
vector: 0.0,
|
|
77
46
|
severity: 0.15,
|
|
78
47
|
recency: 0.15,
|
|
79
48
|
tagOverlap: 0.15,
|
|
80
49
|
domainMatch: 0.15,
|
|
81
50
|
};
|
|
82
51
|
|
|
52
|
+
const COGNEE_WEIGHTS: ScoringWeights = {
|
|
53
|
+
semantic: 0.25,
|
|
54
|
+
vector: 0.35,
|
|
55
|
+
severity: 0.1,
|
|
56
|
+
recency: 0.1,
|
|
57
|
+
tagOverlap: 0.1,
|
|
58
|
+
domainMatch: 0.1,
|
|
59
|
+
};
|
|
60
|
+
|
|
83
61
|
const WEIGHT_BOUND = 0.15;
|
|
84
62
|
const FEEDBACK_THRESHOLD = 30;
|
|
85
63
|
const DUPLICATE_BLOCK_THRESHOLD = 0.8;
|
|
@@ -88,16 +66,18 @@ const RECENCY_HALF_LIFE_DAYS = 365;
|
|
|
88
66
|
|
|
89
67
|
export class Brain {
|
|
90
68
|
private vault: Vault;
|
|
69
|
+
private cognee: CogneeClient | undefined;
|
|
91
70
|
private vocabulary: Map<string, number> = new Map();
|
|
92
71
|
private weights: ScoringWeights = { ...DEFAULT_WEIGHTS };
|
|
93
72
|
|
|
94
|
-
constructor(vault: Vault) {
|
|
73
|
+
constructor(vault: Vault, cognee?: CogneeClient) {
|
|
95
74
|
this.vault = vault;
|
|
75
|
+
this.cognee = cognee;
|
|
96
76
|
this.rebuildVocabulary();
|
|
97
77
|
this.recomputeWeights();
|
|
98
78
|
}
|
|
99
79
|
|
|
100
|
-
intelligentSearch(query: string, options?: SearchOptions): RankedResult[] {
|
|
80
|
+
async intelligentSearch(query: string, options?: SearchOptions): Promise<RankedResult[]> {
|
|
101
81
|
const limit = options?.limit ?? 10;
|
|
102
82
|
const rawResults = this.vault.search(query, {
|
|
103
83
|
domain: options?.domain,
|
|
@@ -106,6 +86,97 @@ export class Brain {
|
|
|
106
86
|
limit: Math.max(limit * 3, 30),
|
|
107
87
|
});
|
|
108
88
|
|
|
89
|
+
// Cognee vector search (parallel, with timeout fallback)
|
|
90
|
+
let cogneeScoreMap: Map<string, number> = new Map();
|
|
91
|
+
const cogneeAvailable = this.cognee?.isAvailable ?? false;
|
|
92
|
+
if (cogneeAvailable && this.cognee) {
|
|
93
|
+
try {
|
|
94
|
+
const cogneeResults = await this.cognee.search(query, { limit: Math.max(limit * 2, 20) });
|
|
95
|
+
|
|
96
|
+
// Build title → entryIds reverse index from FTS results for text-based matching.
|
|
97
|
+
// Cognee assigns its own UUIDs to chunks and may strip embedded metadata during
|
|
98
|
+
// chunking, so we need multiple strategies to cross-reference results.
|
|
99
|
+
// Multiple entries can share a title, so map to arrays of IDs.
|
|
100
|
+
const titleToIds = new Map<string, string[]>();
|
|
101
|
+
for (const r of rawResults) {
|
|
102
|
+
const key = r.entry.title.toLowerCase().trim();
|
|
103
|
+
const ids = titleToIds.get(key) ?? [];
|
|
104
|
+
ids.push(r.entry.id);
|
|
105
|
+
titleToIds.set(key, ids);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const vaultIdPattern = /\[vault-id:([^\]]+)\]/;
|
|
109
|
+
const unmatchedCogneeResults: Array<{ text: string; score: number }> = [];
|
|
110
|
+
|
|
111
|
+
for (const cr of cogneeResults) {
|
|
112
|
+
const text = cr.text ?? '';
|
|
113
|
+
|
|
114
|
+
// Strategy 1: Extract vault ID from [vault-id:XXX] prefix (if Cognee preserved it)
|
|
115
|
+
const vaultIdMatch = text.match(vaultIdPattern);
|
|
116
|
+
if (vaultIdMatch) {
|
|
117
|
+
const vaultId = vaultIdMatch[1];
|
|
118
|
+
cogneeScoreMap.set(vaultId, Math.max(cogneeScoreMap.get(vaultId) ?? 0, cr.score));
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Strategy 2: Match first line of chunk text against known entry titles.
|
|
123
|
+
// serializeEntry() puts the title on the first line after the [vault-id:] prefix,
|
|
124
|
+
// and Cognee's chunking typically preserves this as the chunk start.
|
|
125
|
+
const firstLine = text.split('\n')[0]?.trim().toLowerCase() ?? '';
|
|
126
|
+
const matchedIds = firstLine ? titleToIds.get(firstLine) : undefined;
|
|
127
|
+
if (matchedIds) {
|
|
128
|
+
for (const id of matchedIds) {
|
|
129
|
+
cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, cr.score));
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Strategy 3: Check if any known title appears as a substring in the chunk.
|
|
135
|
+
// Handles cases where the title isn't on the first line (mid-document chunks).
|
|
136
|
+
const textLower = text.toLowerCase();
|
|
137
|
+
let found = false;
|
|
138
|
+
for (const [title, ids] of titleToIds) {
|
|
139
|
+
if (title.length >= 8 && textLower.includes(title)) {
|
|
140
|
+
for (const id of ids) {
|
|
141
|
+
cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, cr.score));
|
|
142
|
+
}
|
|
143
|
+
found = true;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (!found && text.length > 0) {
|
|
148
|
+
unmatchedCogneeResults.push({ text, score: cr.score });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Strategy 4: For Cognee-only semantic matches (not in FTS results),
|
|
153
|
+
// use the first line as a vault FTS query to find the source entry.
|
|
154
|
+
// Preserve caller filters (domain/type/severity) to avoid reintroducing
|
|
155
|
+
// entries the original query excluded.
|
|
156
|
+
for (const unmatched of unmatchedCogneeResults) {
|
|
157
|
+
const searchTerm = unmatched.text.split('\n')[0]?.trim();
|
|
158
|
+
if (!searchTerm || searchTerm.length < 3) continue;
|
|
159
|
+
const vaultHits = this.vault.search(searchTerm, {
|
|
160
|
+
domain: options?.domain,
|
|
161
|
+
type: options?.type,
|
|
162
|
+
severity: options?.severity,
|
|
163
|
+
limit: 1,
|
|
164
|
+
});
|
|
165
|
+
if (vaultHits.length > 0) {
|
|
166
|
+
const id = vaultHits[0].entry.id;
|
|
167
|
+
cogneeScoreMap.set(id, Math.max(cogneeScoreMap.get(id) ?? 0, unmatched.score));
|
|
168
|
+
// Also add to FTS results pool if not already present
|
|
169
|
+
if (!rawResults.some((r) => r.entry.id === id)) {
|
|
170
|
+
rawResults.push(vaultHits[0]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Cognee failed — fall back to FTS5 only
|
|
176
|
+
cogneeScoreMap = new Map();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
109
180
|
if (rawResults.length === 0) return [];
|
|
110
181
|
|
|
111
182
|
const queryTokens = tokenize(query);
|
|
@@ -113,9 +184,22 @@ export class Brain {
|
|
|
113
184
|
const queryDomain = options?.domain;
|
|
114
185
|
const now = Math.floor(Date.now() / 1000);
|
|
115
186
|
|
|
187
|
+
// Use cognee-aware weights only if at least one ranked candidate has a vector score
|
|
188
|
+
const hasVectorCandidate = rawResults.some((r) => cogneeScoreMap.has(r.entry.id));
|
|
189
|
+
const activeWeights = hasVectorCandidate ? this.getCogneeWeights() : this.weights;
|
|
190
|
+
|
|
116
191
|
const ranked = rawResults.map((result) => {
|
|
117
192
|
const entry = result.entry;
|
|
118
|
-
const
|
|
193
|
+
const vectorScore = cogneeScoreMap.get(entry.id) ?? 0;
|
|
194
|
+
const breakdown = this.scoreEntry(
|
|
195
|
+
entry,
|
|
196
|
+
queryTokens,
|
|
197
|
+
queryTags,
|
|
198
|
+
queryDomain,
|
|
199
|
+
now,
|
|
200
|
+
vectorScore,
|
|
201
|
+
activeWeights,
|
|
202
|
+
);
|
|
119
203
|
return { entry, score: breakdown.total, breakdown };
|
|
120
204
|
});
|
|
121
205
|
|
|
@@ -166,6 +250,11 @@ export class Brain {
|
|
|
166
250
|
this.vault.add(fullEntry);
|
|
167
251
|
this.updateVocabularyIncremental(fullEntry);
|
|
168
252
|
|
|
253
|
+
// Fire-and-forget Cognee sync
|
|
254
|
+
if (this.cognee?.isAvailable) {
|
|
255
|
+
this.cognee.addEntries([fullEntry]).catch(() => {});
|
|
256
|
+
}
|
|
257
|
+
|
|
169
258
|
const result: CaptureResult = {
|
|
170
259
|
captured: true,
|
|
171
260
|
id: entry.id,
|
|
@@ -189,13 +278,39 @@ export class Brain {
|
|
|
189
278
|
this.recomputeWeights();
|
|
190
279
|
}
|
|
191
280
|
|
|
192
|
-
getRelevantPatterns(context: QueryContext): RankedResult[] {
|
|
281
|
+
async getRelevantPatterns(context: QueryContext): Promise<RankedResult[]> {
|
|
193
282
|
return this.intelligentSearch(context.query, {
|
|
194
283
|
domain: context.domain,
|
|
195
284
|
tags: context.tags,
|
|
196
285
|
});
|
|
197
286
|
}
|
|
198
287
|
|
|
288
|
+
async syncToCognee(): Promise<{ synced: number; cognified: boolean }> {
|
|
289
|
+
if (!this.cognee?.isAvailable) return { synced: 0, cognified: false };
|
|
290
|
+
|
|
291
|
+
const batchSize = 1000;
|
|
292
|
+
let offset = 0;
|
|
293
|
+
let totalSynced = 0;
|
|
294
|
+
|
|
295
|
+
while (true) {
|
|
296
|
+
const batch = this.vault.list({ limit: batchSize, offset });
|
|
297
|
+
if (batch.length === 0) break;
|
|
298
|
+
|
|
299
|
+
const { added } = await this.cognee.addEntries(batch);
|
|
300
|
+
totalSynced += added;
|
|
301
|
+
offset += batch.length;
|
|
302
|
+
|
|
303
|
+
if (batch.length < batchSize) break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (totalSynced === 0) return { synced: 0, cognified: false };
|
|
307
|
+
|
|
308
|
+
let cognified = false;
|
|
309
|
+
const cognifyResult = await this.cognee.cognify();
|
|
310
|
+
cognified = cognifyResult.status === 'ok';
|
|
311
|
+
return { synced: totalSynced, cognified };
|
|
312
|
+
}
|
|
313
|
+
|
|
199
314
|
rebuildVocabulary(): void {
|
|
200
315
|
const entries = this.vault.list({ limit: 100000 });
|
|
201
316
|
const docCount = entries.length;
|
|
@@ -249,7 +364,11 @@ export class Brain {
|
|
|
249
364
|
queryTags: string[],
|
|
250
365
|
queryDomain: string | undefined,
|
|
251
366
|
now: number,
|
|
367
|
+
vectorScore: number = 0,
|
|
368
|
+
activeWeights?: ScoringWeights,
|
|
252
369
|
): ScoreBreakdown {
|
|
370
|
+
const w = activeWeights ?? this.weights;
|
|
371
|
+
|
|
253
372
|
let semantic = 0;
|
|
254
373
|
if (this.vocabulary.size > 0 && queryTokens.length > 0) {
|
|
255
374
|
const entryText = [
|
|
@@ -274,14 +393,17 @@ export class Brain {
|
|
|
274
393
|
|
|
275
394
|
const domainMatch = queryDomain && entry.domain === queryDomain ? 1.0 : 0;
|
|
276
395
|
|
|
277
|
-
const
|
|
278
|
-
this.weights.semantic * semantic +
|
|
279
|
-
this.weights.severity * severity +
|
|
280
|
-
this.weights.recency * recency +
|
|
281
|
-
this.weights.tagOverlap * tagOverlap +
|
|
282
|
-
this.weights.domainMatch * domainMatch;
|
|
396
|
+
const vector = vectorScore;
|
|
283
397
|
|
|
284
|
-
|
|
398
|
+
const total =
|
|
399
|
+
w.semantic * semantic +
|
|
400
|
+
w.vector * vector +
|
|
401
|
+
w.severity * severity +
|
|
402
|
+
w.recency * recency +
|
|
403
|
+
w.tagOverlap * tagOverlap +
|
|
404
|
+
w.domainMatch * domainMatch;
|
|
405
|
+
|
|
406
|
+
return { semantic, vector, severity, recency, tagOverlap, domainMatch, total };
|
|
285
407
|
}
|
|
286
408
|
|
|
287
409
|
private generateTags(title: string, description: string, context?: string): string[] {
|
|
@@ -375,6 +497,10 @@ export class Brain {
|
|
|
375
497
|
tx();
|
|
376
498
|
}
|
|
377
499
|
|
|
500
|
+
private getCogneeWeights(): ScoringWeights {
|
|
501
|
+
return { ...COGNEE_WEIGHTS };
|
|
502
|
+
}
|
|
503
|
+
|
|
378
504
|
private recomputeWeights(): void {
|
|
379
505
|
const db = this.vault.getDb();
|
|
380
506
|
const feedbackCount = (
|
|
@@ -401,7 +527,10 @@ export class Brain {
|
|
|
401
527
|
DEFAULT_WEIGHTS.semantic + WEIGHT_BOUND,
|
|
402
528
|
);
|
|
403
529
|
|
|
404
|
-
|
|
530
|
+
// vector stays 0 in base weights (only active during hybrid search)
|
|
531
|
+
newWeights.vector = 0;
|
|
532
|
+
|
|
533
|
+
const remaining = 1.0 - newWeights.semantic - newWeights.vector;
|
|
405
534
|
const otherSum =
|
|
406
535
|
DEFAULT_WEIGHTS.severity +
|
|
407
536
|
DEFAULT_WEIGHTS.recency +
|