@akashabot/openclaw-memory-offline-core 0.2.0 → 0.4.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/index.d.ts +129 -0
- package/dist/index.js +426 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,19 @@ export type MemItem = {
|
|
|
22
22
|
process_id: string | null;
|
|
23
23
|
session_id: string | null;
|
|
24
24
|
};
|
|
25
|
+
export type Fact = {
|
|
26
|
+
id: string;
|
|
27
|
+
created_at: number;
|
|
28
|
+
subject: string;
|
|
29
|
+
predicate: string;
|
|
30
|
+
object: string;
|
|
31
|
+
confidence: number;
|
|
32
|
+
source_item_id: string | null;
|
|
33
|
+
entity_id: string | null;
|
|
34
|
+
};
|
|
35
|
+
export type InsertFactInput = Omit<Fact, 'created_at'> & {
|
|
36
|
+
created_at?: number;
|
|
37
|
+
};
|
|
25
38
|
export type InsertItemInput = Omit<MemItem, 'created_at'> & {
|
|
26
39
|
created_at?: number;
|
|
27
40
|
};
|
|
@@ -106,3 +119,119 @@ export declare function listEntities(db: Database.Database): string[];
|
|
|
106
119
|
* List distinct session_ids in the database.
|
|
107
120
|
*/
|
|
108
121
|
export declare function listSessions(db: Database.Database): string[];
|
|
122
|
+
/**
|
|
123
|
+
* Insert a new fact into the database.
|
|
124
|
+
*/
|
|
125
|
+
export declare function insertFact(db: Database.Database, input: InsertFactInput): Fact;
|
|
126
|
+
/**
|
|
127
|
+
* Get all facts about a specific subject.
|
|
128
|
+
*/
|
|
129
|
+
export declare function getFactsBySubject(db: Database.Database, subject: string, limit?: number): Fact[];
|
|
130
|
+
/**
|
|
131
|
+
* Get all facts with a specific predicate.
|
|
132
|
+
*/
|
|
133
|
+
export declare function getFactsByPredicate(db: Database.Database, predicate: string, limit?: number): Fact[];
|
|
134
|
+
/**
|
|
135
|
+
* Search facts by subject, predicate, or object (simple LIKE search).
|
|
136
|
+
*/
|
|
137
|
+
export declare function searchFacts(db: Database.Database, query: string, limit?: number): Fact[];
|
|
138
|
+
/**
|
|
139
|
+
* Get all facts (optionally filtered by entity_id).
|
|
140
|
+
*/
|
|
141
|
+
export declare function getAllFacts(db: Database.Database, entityId?: string, limit?: number): Fact[];
|
|
142
|
+
/**
|
|
143
|
+
* List distinct subjects in the facts table.
|
|
144
|
+
*/
|
|
145
|
+
export declare function listSubjects(db: Database.Database): string[];
|
|
146
|
+
/**
|
|
147
|
+
* List distinct predicates in the facts table.
|
|
148
|
+
*/
|
|
149
|
+
export declare function listPredicates(db: Database.Database): string[];
|
|
150
|
+
/**
|
|
151
|
+
* Delete a fact by ID.
|
|
152
|
+
*/
|
|
153
|
+
export declare function deleteFact(db: Database.Database, id: string): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Delete all facts derived from a specific memory item.
|
|
156
|
+
*/
|
|
157
|
+
export declare function deleteFactsBySourceItem(db: Database.Database, sourceItemId: string): number;
|
|
158
|
+
/**
|
|
159
|
+
* Simple pattern-based fact extraction.
|
|
160
|
+
* Looks for common patterns like "X works at Y", "X prefers Y", etc.
|
|
161
|
+
* Returns an array of potential facts (not inserted yet).
|
|
162
|
+
*/
|
|
163
|
+
export declare function extractFactsSimple(text: string, entityId?: string): Array<{
|
|
164
|
+
subject: string;
|
|
165
|
+
predicate: string;
|
|
166
|
+
object: string;
|
|
167
|
+
confidence: number;
|
|
168
|
+
}>;
|
|
169
|
+
export type GraphEdge = {
|
|
170
|
+
subject: string;
|
|
171
|
+
predicate: string;
|
|
172
|
+
object: string;
|
|
173
|
+
confidence: number;
|
|
174
|
+
};
|
|
175
|
+
export type GraphPath = {
|
|
176
|
+
from: string;
|
|
177
|
+
to: string;
|
|
178
|
+
path: Array<{
|
|
179
|
+
entity: string;
|
|
180
|
+
edge?: GraphEdge;
|
|
181
|
+
}>;
|
|
182
|
+
length: number;
|
|
183
|
+
};
|
|
184
|
+
export type GraphStats = {
|
|
185
|
+
totalFacts: number;
|
|
186
|
+
totalEntities: number;
|
|
187
|
+
totalPredicates: number;
|
|
188
|
+
avgConnectionsPerEntity: number;
|
|
189
|
+
mostConnectedEntities: Array<{
|
|
190
|
+
entity: string;
|
|
191
|
+
connections: number;
|
|
192
|
+
}>;
|
|
193
|
+
mostUsedPredicates: Array<{
|
|
194
|
+
predicate: string;
|
|
195
|
+
count: number;
|
|
196
|
+
}>;
|
|
197
|
+
};
|
|
198
|
+
/**
|
|
199
|
+
* Get all facts where the entity is either subject or object.
|
|
200
|
+
*/
|
|
201
|
+
export declare function getEntityGraph(db: Database.Database, entity: string): GraphEdge[];
|
|
202
|
+
/**
|
|
203
|
+
* Get all entities directly connected to a given entity.
|
|
204
|
+
*/
|
|
205
|
+
export declare function getRelatedEntities(db: Database.Database, entity: string): string[];
|
|
206
|
+
/**
|
|
207
|
+
* Find paths between two entities using BFS (breadth-first search).
|
|
208
|
+
* Returns up to `maxPaths` paths with a maximum depth of `maxDepth`.
|
|
209
|
+
*/
|
|
210
|
+
export declare function findPaths(db: Database.Database, fromEntity: string, toEntity: string, maxDepth?: number, maxPaths?: number): GraphPath[];
|
|
211
|
+
/**
|
|
212
|
+
* Get statistics about the knowledge graph.
|
|
213
|
+
*/
|
|
214
|
+
export declare function getGraphStats(db: Database.Database): GraphStats;
|
|
215
|
+
/**
|
|
216
|
+
* Export the graph as JSON (for visualization tools).
|
|
217
|
+
*/
|
|
218
|
+
export declare function exportGraphJson(db: Database.Database, options?: {
|
|
219
|
+
limit?: number;
|
|
220
|
+
minConfidence?: number;
|
|
221
|
+
entity?: string;
|
|
222
|
+
}): {
|
|
223
|
+
nodes: Array<{
|
|
224
|
+
id: string;
|
|
225
|
+
label: string;
|
|
226
|
+
}>;
|
|
227
|
+
edges: Array<{
|
|
228
|
+
from: string;
|
|
229
|
+
to: string;
|
|
230
|
+
label: string;
|
|
231
|
+
confidence: number;
|
|
232
|
+
}>;
|
|
233
|
+
};
|
|
234
|
+
/**
|
|
235
|
+
* Find all entities matching a pattern (LIKE search).
|
|
236
|
+
*/
|
|
237
|
+
export declare function searchEntities(db: Database.Database, pattern: string, limit?: number): string[];
|
package/dist/index.js
CHANGED
|
@@ -57,10 +57,28 @@ export function initSchema(db) {
|
|
|
57
57
|
FOREIGN KEY(item_id) REFERENCES items(id)
|
|
58
58
|
);
|
|
59
59
|
|
|
60
|
+
-- Phase 2: Structured Facts Table
|
|
61
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
62
|
+
id TEXT PRIMARY KEY,
|
|
63
|
+
created_at INTEGER NOT NULL,
|
|
64
|
+
subject TEXT NOT NULL,
|
|
65
|
+
predicate TEXT NOT NULL,
|
|
66
|
+
object TEXT NOT NULL,
|
|
67
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
68
|
+
source_item_id TEXT,
|
|
69
|
+
entity_id TEXT,
|
|
70
|
+
FOREIGN KEY(source_item_id) REFERENCES items(id)
|
|
71
|
+
);
|
|
72
|
+
|
|
60
73
|
-- Indexes for Phase 1: Attribution & Session filtering
|
|
61
74
|
CREATE INDEX IF NOT EXISTS idx_items_entity_id ON items(entity_id);
|
|
62
75
|
CREATE INDEX IF NOT EXISTS idx_items_process_id ON items(process_id);
|
|
63
76
|
CREATE INDEX IF NOT EXISTS idx_items_session_id ON items(session_id);
|
|
77
|
+
|
|
78
|
+
-- Indexes for Phase 2: Facts queries
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_facts_subject ON facts(subject);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_facts_predicate ON facts(predicate);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_facts_entity_id ON facts(entity_id);
|
|
64
82
|
`);
|
|
65
83
|
}
|
|
66
84
|
/**
|
|
@@ -480,3 +498,411 @@ export function listSessions(db) {
|
|
|
480
498
|
.all();
|
|
481
499
|
return rows.map(r => r.session_id);
|
|
482
500
|
}
|
|
501
|
+
// ============================================================================
|
|
502
|
+
// Phase 2: Structured Facts
|
|
503
|
+
// ============================================================================
|
|
504
|
+
/**
|
|
505
|
+
* Insert a new fact into the database.
|
|
506
|
+
*/
|
|
507
|
+
export function insertFact(db, input) {
|
|
508
|
+
const stmt = db.prepare(`
|
|
509
|
+
INSERT INTO facts (id, created_at, subject, predicate, object, confidence, source_item_id, entity_id)
|
|
510
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
511
|
+
`);
|
|
512
|
+
const created_at = input.created_at ?? Date.now();
|
|
513
|
+
stmt.run(input.id, created_at, input.subject, input.predicate, input.object, input.confidence, input.source_item_id ?? null, input.entity_id ?? null);
|
|
514
|
+
return { ...input, created_at };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Get all facts about a specific subject.
|
|
518
|
+
*/
|
|
519
|
+
export function getFactsBySubject(db, subject, limit = 100) {
|
|
520
|
+
const rows = db
|
|
521
|
+
.prepare(`SELECT id, created_at, subject, predicate, object, confidence, source_item_id, entity_id
|
|
522
|
+
FROM facts
|
|
523
|
+
WHERE subject = ?
|
|
524
|
+
ORDER BY confidence DESC, created_at DESC
|
|
525
|
+
LIMIT ?`)
|
|
526
|
+
.all(subject, limit);
|
|
527
|
+
return rows.map(r => ({
|
|
528
|
+
id: r.id,
|
|
529
|
+
created_at: r.created_at,
|
|
530
|
+
subject: r.subject,
|
|
531
|
+
predicate: r.predicate,
|
|
532
|
+
object: r.object,
|
|
533
|
+
confidence: r.confidence,
|
|
534
|
+
source_item_id: r.source_item_id,
|
|
535
|
+
entity_id: r.entity_id,
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Get all facts with a specific predicate.
|
|
540
|
+
*/
|
|
541
|
+
export function getFactsByPredicate(db, predicate, limit = 100) {
|
|
542
|
+
const rows = db
|
|
543
|
+
.prepare(`SELECT id, created_at, subject, predicate, object, confidence, source_item_id, entity_id
|
|
544
|
+
FROM facts
|
|
545
|
+
WHERE predicate = ?
|
|
546
|
+
ORDER BY confidence DESC, created_at DESC
|
|
547
|
+
LIMIT ?`)
|
|
548
|
+
.all(predicate, limit);
|
|
549
|
+
return rows.map(r => ({
|
|
550
|
+
id: r.id,
|
|
551
|
+
created_at: r.created_at,
|
|
552
|
+
subject: r.subject,
|
|
553
|
+
predicate: r.predicate,
|
|
554
|
+
object: r.object,
|
|
555
|
+
confidence: r.confidence,
|
|
556
|
+
source_item_id: r.source_item_id,
|
|
557
|
+
entity_id: r.entity_id,
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Search facts by subject, predicate, or object (simple LIKE search).
|
|
562
|
+
*/
|
|
563
|
+
export function searchFacts(db, query, limit = 50) {
|
|
564
|
+
const pattern = `%${query}%`;
|
|
565
|
+
const rows = db
|
|
566
|
+
.prepare(`SELECT id, created_at, subject, predicate, object, confidence, source_item_id, entity_id
|
|
567
|
+
FROM facts
|
|
568
|
+
WHERE subject LIKE ? OR predicate LIKE ? OR object LIKE ?
|
|
569
|
+
ORDER BY confidence DESC, created_at DESC
|
|
570
|
+
LIMIT ?`)
|
|
571
|
+
.all(pattern, pattern, pattern, limit);
|
|
572
|
+
return rows.map(r => ({
|
|
573
|
+
id: r.id,
|
|
574
|
+
created_at: r.created_at,
|
|
575
|
+
subject: r.subject,
|
|
576
|
+
predicate: r.predicate,
|
|
577
|
+
object: r.object,
|
|
578
|
+
confidence: r.confidence,
|
|
579
|
+
source_item_id: r.source_item_id,
|
|
580
|
+
entity_id: r.entity_id,
|
|
581
|
+
}));
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Get all facts (optionally filtered by entity_id).
|
|
585
|
+
*/
|
|
586
|
+
export function getAllFacts(db, entityId, limit = 100) {
|
|
587
|
+
let rows;
|
|
588
|
+
if (entityId) {
|
|
589
|
+
rows = db
|
|
590
|
+
.prepare(`SELECT id, created_at, subject, predicate, object, confidence, source_item_id, entity_id
|
|
591
|
+
FROM facts
|
|
592
|
+
WHERE entity_id = ?
|
|
593
|
+
ORDER BY created_at DESC
|
|
594
|
+
LIMIT ?`)
|
|
595
|
+
.all(entityId, limit);
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
rows = db
|
|
599
|
+
.prepare(`SELECT id, created_at, subject, predicate, object, confidence, source_item_id, entity_id
|
|
600
|
+
FROM facts
|
|
601
|
+
ORDER BY created_at DESC
|
|
602
|
+
LIMIT ?`)
|
|
603
|
+
.all(limit);
|
|
604
|
+
}
|
|
605
|
+
return rows.map(r => ({
|
|
606
|
+
id: r.id,
|
|
607
|
+
created_at: r.created_at,
|
|
608
|
+
subject: r.subject,
|
|
609
|
+
predicate: r.predicate,
|
|
610
|
+
object: r.object,
|
|
611
|
+
confidence: r.confidence,
|
|
612
|
+
source_item_id: r.source_item_id,
|
|
613
|
+
entity_id: r.entity_id,
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* List distinct subjects in the facts table.
|
|
618
|
+
*/
|
|
619
|
+
export function listSubjects(db) {
|
|
620
|
+
const rows = db
|
|
621
|
+
.prepare(`SELECT DISTINCT subject FROM facts ORDER BY subject`)
|
|
622
|
+
.all();
|
|
623
|
+
return rows.map(r => r.subject);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* List distinct predicates in the facts table.
|
|
627
|
+
*/
|
|
628
|
+
export function listPredicates(db) {
|
|
629
|
+
const rows = db
|
|
630
|
+
.prepare(`SELECT DISTINCT predicate FROM facts ORDER BY predicate`)
|
|
631
|
+
.all();
|
|
632
|
+
return rows.map(r => r.predicate);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Delete a fact by ID.
|
|
636
|
+
*/
|
|
637
|
+
export function deleteFact(db, id) {
|
|
638
|
+
const stmt = db.prepare('DELETE FROM facts WHERE id = ?');
|
|
639
|
+
const result = stmt.run(id);
|
|
640
|
+
return (result.changes ?? 0) > 0;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Delete all facts derived from a specific memory item.
|
|
644
|
+
*/
|
|
645
|
+
export function deleteFactsBySourceItem(db, sourceItemId) {
|
|
646
|
+
const stmt = db.prepare('DELETE FROM facts WHERE source_item_id = ?');
|
|
647
|
+
const result = stmt.run(sourceItemId);
|
|
648
|
+
return result.changes ?? 0;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Simple pattern-based fact extraction.
|
|
652
|
+
* Looks for common patterns like "X works at Y", "X prefers Y", etc.
|
|
653
|
+
* Returns an array of potential facts (not inserted yet).
|
|
654
|
+
*/
|
|
655
|
+
export function extractFactsSimple(text, entityId) {
|
|
656
|
+
const facts = [];
|
|
657
|
+
const lower = text.toLowerCase();
|
|
658
|
+
// Pattern: "X works at Y" / "X travaille chez Y"
|
|
659
|
+
const workPatterns = [
|
|
660
|
+
/(\w+)\s+(?:works at|travaille chez|works for|work at)\s+([\w\s]+?)(?:\.|,|$)/gi,
|
|
661
|
+
];
|
|
662
|
+
for (const pattern of workPatterns) {
|
|
663
|
+
let match;
|
|
664
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
665
|
+
facts.push({
|
|
666
|
+
subject: match[1].trim(),
|
|
667
|
+
predicate: 'works_at',
|
|
668
|
+
object: match[2].trim(),
|
|
669
|
+
confidence: 0.7,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
// Pattern: "X prefers Y" / "X préfère Y"
|
|
674
|
+
const preferPatterns = [
|
|
675
|
+
/(\w+)\s+(?:prefers?|préfère)\s+([\w\s]+?)(?:\.|,|$)/gi,
|
|
676
|
+
];
|
|
677
|
+
for (const pattern of preferPatterns) {
|
|
678
|
+
let match;
|
|
679
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
680
|
+
facts.push({
|
|
681
|
+
subject: match[1].trim(),
|
|
682
|
+
predicate: 'prefers',
|
|
683
|
+
object: match[2].trim(),
|
|
684
|
+
confidence: 0.8,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// Pattern: "X is Y" / "X est Y"
|
|
689
|
+
const isPatterns = [
|
|
690
|
+
/(\w+)\s+(?:is|est)\s+(?:a |an |un |une )?([\w\s]+?)(?:\.|,|$)/gi,
|
|
691
|
+
];
|
|
692
|
+
for (const pattern of isPatterns) {
|
|
693
|
+
let match;
|
|
694
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
695
|
+
const subject = match[1].trim();
|
|
696
|
+
// Skip common false positives
|
|
697
|
+
if (['it', 'this', 'that', 'ce', 'il', 'elle', 'cette'].includes(subject.toLowerCase()))
|
|
698
|
+
continue;
|
|
699
|
+
facts.push({
|
|
700
|
+
subject,
|
|
701
|
+
predicate: 'is',
|
|
702
|
+
object: match[2].trim(),
|
|
703
|
+
confidence: 0.6,
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Dedupe by subject+predicate+object
|
|
708
|
+
const seen = new Set();
|
|
709
|
+
return facts.filter(f => {
|
|
710
|
+
const key = `${f.subject}|${f.predicate}|${f.object}`.toLowerCase();
|
|
711
|
+
if (seen.has(key))
|
|
712
|
+
return false;
|
|
713
|
+
seen.add(key);
|
|
714
|
+
return true;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Get all facts where the entity is either subject or object.
|
|
719
|
+
*/
|
|
720
|
+
export function getEntityGraph(db, entity) {
|
|
721
|
+
const rows = db
|
|
722
|
+
.prepare(`SELECT subject, predicate, object, confidence
|
|
723
|
+
FROM facts
|
|
724
|
+
WHERE subject = ? OR object = ?
|
|
725
|
+
ORDER BY confidence DESC`)
|
|
726
|
+
.all(entity, entity);
|
|
727
|
+
return rows.map(r => ({
|
|
728
|
+
subject: r.subject,
|
|
729
|
+
predicate: r.predicate,
|
|
730
|
+
object: r.object,
|
|
731
|
+
confidence: r.confidence,
|
|
732
|
+
}));
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get all entities directly connected to a given entity.
|
|
736
|
+
*/
|
|
737
|
+
export function getRelatedEntities(db, entity) {
|
|
738
|
+
const rows = db
|
|
739
|
+
.prepare(`SELECT DISTINCT CASE
|
|
740
|
+
WHEN subject = ? THEN object
|
|
741
|
+
ELSE subject
|
|
742
|
+
END AS related
|
|
743
|
+
FROM facts
|
|
744
|
+
WHERE subject = ? OR object = ?`)
|
|
745
|
+
.all(entity, entity, entity);
|
|
746
|
+
return rows.map(r => r.related).filter(e => e !== entity);
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Find paths between two entities using BFS (breadth-first search).
|
|
750
|
+
* Returns up to `maxPaths` paths with a maximum depth of `maxDepth`.
|
|
751
|
+
*/
|
|
752
|
+
export function findPaths(db, fromEntity, toEntity, maxDepth = 4, maxPaths = 5) {
|
|
753
|
+
// Get all edges for efficient graph traversal
|
|
754
|
+
const allEdges = db
|
|
755
|
+
.prepare(`SELECT subject, predicate, object, confidence FROM facts`)
|
|
756
|
+
.all();
|
|
757
|
+
// Build adjacency list (undirected - we can traverse both ways)
|
|
758
|
+
const adjacency = new Map();
|
|
759
|
+
for (const e of allEdges) {
|
|
760
|
+
const edge = {
|
|
761
|
+
subject: e.subject,
|
|
762
|
+
predicate: e.predicate,
|
|
763
|
+
object: e.object,
|
|
764
|
+
confidence: e.confidence,
|
|
765
|
+
};
|
|
766
|
+
if (!adjacency.has(e.subject))
|
|
767
|
+
adjacency.set(e.subject, []);
|
|
768
|
+
if (!adjacency.has(e.object))
|
|
769
|
+
adjacency.set(e.object, []);
|
|
770
|
+
adjacency.get(e.subject).push({ entity: e.object, edge });
|
|
771
|
+
adjacency.get(e.object).push({ entity: e.subject, edge });
|
|
772
|
+
}
|
|
773
|
+
// BFS to find paths
|
|
774
|
+
const paths = [];
|
|
775
|
+
const queue = [
|
|
776
|
+
{ entity: fromEntity, path: [{ entity: fromEntity }] }
|
|
777
|
+
];
|
|
778
|
+
const visited = new Set();
|
|
779
|
+
while (queue.length > 0 && paths.length < maxPaths) {
|
|
780
|
+
const current = queue.shift();
|
|
781
|
+
if (current.entity === toEntity && current.path.length > 1) {
|
|
782
|
+
paths.push({
|
|
783
|
+
from: fromEntity,
|
|
784
|
+
to: toEntity,
|
|
785
|
+
path: current.path,
|
|
786
|
+
length: current.path.length - 1,
|
|
787
|
+
});
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (current.path.length > maxDepth)
|
|
791
|
+
continue;
|
|
792
|
+
const neighbors = adjacency.get(current.entity) || [];
|
|
793
|
+
for (const neighbor of neighbors) {
|
|
794
|
+
const pathKey = `${current.entity}|${neighbor.entity}`;
|
|
795
|
+
if (visited.has(pathKey))
|
|
796
|
+
continue;
|
|
797
|
+
visited.add(pathKey);
|
|
798
|
+
queue.push({
|
|
799
|
+
entity: neighbor.entity,
|
|
800
|
+
path: [...current.path, { entity: neighbor.entity, edge: neighbor.edge }],
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return paths.sort((a, b) => a.length - b.length);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get statistics about the knowledge graph.
|
|
808
|
+
*/
|
|
809
|
+
export function getGraphStats(db) {
|
|
810
|
+
// Total facts
|
|
811
|
+
const totalFactsRow = db.prepare(`SELECT COUNT(*) as count FROM facts`).get();
|
|
812
|
+
const totalFacts = totalFactsRow?.count ?? 0;
|
|
813
|
+
// Total unique entities (subjects + objects)
|
|
814
|
+
const entitiesRow = db
|
|
815
|
+
.prepare(`SELECT COUNT(DISTINCT entity) as count FROM (
|
|
816
|
+
SELECT subject as entity FROM facts
|
|
817
|
+
UNION
|
|
818
|
+
SELECT object as entity FROM facts
|
|
819
|
+
)`)
|
|
820
|
+
.get();
|
|
821
|
+
const totalEntities = entitiesRow?.count ?? 0;
|
|
822
|
+
// Total predicates
|
|
823
|
+
const predicatesRow = db
|
|
824
|
+
.prepare(`SELECT COUNT(DISTINCT predicate) as count FROM facts`)
|
|
825
|
+
.get();
|
|
826
|
+
const totalPredicates = predicatesRow?.count ?? 0;
|
|
827
|
+
// Most connected entities
|
|
828
|
+
const mostConnected = db
|
|
829
|
+
.prepare(`SELECT entity, COUNT(*) as connections FROM (
|
|
830
|
+
SELECT subject as entity FROM facts
|
|
831
|
+
UNION ALL
|
|
832
|
+
SELECT object as entity FROM facts
|
|
833
|
+
) GROUP BY entity ORDER BY connections DESC LIMIT 10`)
|
|
834
|
+
.all();
|
|
835
|
+
// Most used predicates
|
|
836
|
+
const mostUsedPredicates = db
|
|
837
|
+
.prepare(`SELECT predicate, COUNT(*) as count FROM facts GROUP BY predicate ORDER BY count DESC LIMIT 10`)
|
|
838
|
+
.all();
|
|
839
|
+
// Average connections per entity
|
|
840
|
+
const avgConnections = totalEntities > 0
|
|
841
|
+
? Math.round((totalFacts * 2 / totalEntities) * 10) / 10
|
|
842
|
+
: 0;
|
|
843
|
+
return {
|
|
844
|
+
totalFacts,
|
|
845
|
+
totalEntities,
|
|
846
|
+
totalPredicates,
|
|
847
|
+
avgConnectionsPerEntity: avgConnections,
|
|
848
|
+
mostConnectedEntities: mostConnected,
|
|
849
|
+
mostUsedPredicates: mostUsedPredicates,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Export the graph as JSON (for visualization tools).
|
|
854
|
+
*/
|
|
855
|
+
export function exportGraphJson(db, options) {
|
|
856
|
+
let facts;
|
|
857
|
+
if (options?.entity) {
|
|
858
|
+
facts = db
|
|
859
|
+
.prepare(`SELECT subject, predicate, object, confidence FROM facts
|
|
860
|
+
WHERE subject = ? OR object = ?
|
|
861
|
+
ORDER BY confidence DESC
|
|
862
|
+
LIMIT ?`)
|
|
863
|
+
.all(options.entity, options.entity, options?.limit ?? 1000);
|
|
864
|
+
}
|
|
865
|
+
else if (options?.minConfidence) {
|
|
866
|
+
facts = db
|
|
867
|
+
.prepare(`SELECT subject, predicate, object, confidence FROM facts
|
|
868
|
+
WHERE confidence >= ?
|
|
869
|
+
ORDER BY confidence DESC
|
|
870
|
+
LIMIT ?`)
|
|
871
|
+
.all(options.minConfidence, options?.limit ?? 1000);
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
facts = db
|
|
875
|
+
.prepare(`SELECT subject, predicate, object, confidence FROM facts
|
|
876
|
+
ORDER BY confidence DESC
|
|
877
|
+
LIMIT ?`)
|
|
878
|
+
.all(options?.limit ?? 1000);
|
|
879
|
+
}
|
|
880
|
+
// Build unique nodes
|
|
881
|
+
const nodeSet = new Set();
|
|
882
|
+
for (const f of facts) {
|
|
883
|
+
nodeSet.add(f.subject);
|
|
884
|
+
nodeSet.add(f.object);
|
|
885
|
+
}
|
|
886
|
+
const nodes = Array.from(nodeSet).map(id => ({ id, label: id }));
|
|
887
|
+
const edges = facts.map(f => ({
|
|
888
|
+
from: f.subject,
|
|
889
|
+
to: f.object,
|
|
890
|
+
label: f.predicate,
|
|
891
|
+
confidence: f.confidence,
|
|
892
|
+
}));
|
|
893
|
+
return { nodes, edges };
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Find all entities matching a pattern (LIKE search).
|
|
897
|
+
*/
|
|
898
|
+
export function searchEntities(db, pattern, limit = 50) {
|
|
899
|
+
const likePattern = `%${pattern}%`;
|
|
900
|
+
const rows = db
|
|
901
|
+
.prepare(`SELECT DISTINCT entity FROM (
|
|
902
|
+
SELECT subject as entity FROM facts WHERE subject LIKE ?
|
|
903
|
+
UNION
|
|
904
|
+
SELECT object as entity FROM facts WHERE object LIKE ?
|
|
905
|
+
) LIMIT ?`)
|
|
906
|
+
.all(likePattern, likePattern, limit);
|
|
907
|
+
return rows.map(r => r.entity);
|
|
908
|
+
}
|
package/package.json
CHANGED