@nguyentamdat/mempalace 1.0.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 +127 -0
- package/hooks/README.md +133 -0
- package/hooks/mempal_precompact_hook.sh +35 -0
- package/hooks/mempal_save_hook.sh +80 -0
- package/package.json +36 -0
- package/src/cli.ts +50 -0
- package/src/commands/compress.ts +161 -0
- package/src/commands/init.ts +40 -0
- package/src/commands/mine.ts +51 -0
- package/src/commands/search.ts +23 -0
- package/src/commands/split.ts +20 -0
- package/src/commands/status.ts +12 -0
- package/src/commands/wake-up.ts +20 -0
- package/src/config.ts +111 -0
- package/src/convo-miner.ts +373 -0
- package/src/dialect.ts +921 -0
- package/src/entity-detector.d.ts +25 -0
- package/src/entity-detector.ts +674 -0
- package/src/entity-registry.ts +806 -0
- package/src/general-extractor.ts +487 -0
- package/src/index.ts +5 -0
- package/src/knowledge-graph.ts +461 -0
- package/src/layers.ts +512 -0
- package/src/mcp-server.ts +1034 -0
- package/src/miner.ts +612 -0
- package/src/missing-modules.d.ts +43 -0
- package/src/normalize.ts +374 -0
- package/src/onboarding.ts +485 -0
- package/src/palace-graph.ts +310 -0
- package/src/room-detector-local.ts +415 -0
- package/src/room-detector.d.ts +1 -0
- package/src/room-detector.ts +6 -0
- package/src/searcher.ts +181 -0
- package/src/spellcheck.ts +200 -0
- package/src/split-mega-files.d.ts +8 -0
- package/src/split-mega-files.ts +297 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdirSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_KG_PATH = join(homedir(), ".mempalace", "knowledge.db");
|
|
8
|
+
|
|
9
|
+
export interface Entity {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
type: string;
|
|
13
|
+
properties: Record<string, unknown>;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Triple {
|
|
18
|
+
id: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
predicate: string;
|
|
21
|
+
object: string;
|
|
22
|
+
validFrom: string | null;
|
|
23
|
+
validTo: string | null;
|
|
24
|
+
confidence: number;
|
|
25
|
+
sourceCloset: string | null;
|
|
26
|
+
sourceFile: string | null;
|
|
27
|
+
extractedAt: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface QueryResult {
|
|
31
|
+
direction?: "outgoing" | "incoming";
|
|
32
|
+
subject: string;
|
|
33
|
+
predicate: string;
|
|
34
|
+
object: string;
|
|
35
|
+
validFrom: string | null;
|
|
36
|
+
validTo: string | null;
|
|
37
|
+
confidence?: number;
|
|
38
|
+
sourceCloset?: string | null;
|
|
39
|
+
current: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface StatsResult {
|
|
43
|
+
entities: number;
|
|
44
|
+
triples: number;
|
|
45
|
+
currentFacts: number;
|
|
46
|
+
expiredFacts: number;
|
|
47
|
+
relationshipTypes: string[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface EntityFact {
|
|
51
|
+
full_name?: string;
|
|
52
|
+
type?: string;
|
|
53
|
+
gender?: string;
|
|
54
|
+
birthday?: string;
|
|
55
|
+
parent?: string;
|
|
56
|
+
partner?: string;
|
|
57
|
+
relationship?: string;
|
|
58
|
+
sibling?: string;
|
|
59
|
+
owner?: string;
|
|
60
|
+
interests?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface QueryEntityRow {
|
|
64
|
+
predicate: string;
|
|
65
|
+
valid_from: string | null;
|
|
66
|
+
valid_to: string | null;
|
|
67
|
+
confidence: number;
|
|
68
|
+
source_closet: string | null;
|
|
69
|
+
related_name: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface TimelineRow {
|
|
73
|
+
predicate: string;
|
|
74
|
+
valid_from: string | null;
|
|
75
|
+
valid_to: string | null;
|
|
76
|
+
sub_name: string;
|
|
77
|
+
obj_name: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface CountRow {
|
|
81
|
+
count: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface PredicateRow {
|
|
85
|
+
predicate: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class KnowledgeGraph {
|
|
89
|
+
public readonly dbPath: string;
|
|
90
|
+
private readonly db: Database;
|
|
91
|
+
|
|
92
|
+
constructor(dbPath?: string | null) {
|
|
93
|
+
this.dbPath = dbPath ?? DEFAULT_KG_PATH;
|
|
94
|
+
mkdirSync(dirname(this.dbPath), { recursive: true });
|
|
95
|
+
this.db = new Database(this.dbPath);
|
|
96
|
+
this.initDb();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private initDb(): void {
|
|
100
|
+
this.db.exec(`
|
|
101
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
name TEXT NOT NULL,
|
|
104
|
+
type TEXT DEFAULT 'unknown',
|
|
105
|
+
properties TEXT DEFAULT '{}',
|
|
106
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
CREATE TABLE IF NOT EXISTS triples (
|
|
110
|
+
id TEXT PRIMARY KEY,
|
|
111
|
+
subject TEXT NOT NULL,
|
|
112
|
+
predicate TEXT NOT NULL,
|
|
113
|
+
object TEXT NOT NULL,
|
|
114
|
+
valid_from TEXT,
|
|
115
|
+
valid_to TEXT,
|
|
116
|
+
confidence REAL DEFAULT 1.0,
|
|
117
|
+
source_closet TEXT,
|
|
118
|
+
source_file TEXT,
|
|
119
|
+
extracted_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
120
|
+
FOREIGN KEY (subject) REFERENCES entities(id),
|
|
121
|
+
FOREIGN KEY (object) REFERENCES entities(id)
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
CREATE INDEX IF NOT EXISTS idx_triples_subject ON triples(subject);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_triples_object ON triples(object);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_triples_predicate ON triples(predicate);
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_triples_valid ON triples(valid_from, valid_to);
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private entityId(name: string): string {
|
|
132
|
+
return name.toLowerCase().replaceAll(" ", "_").replaceAll("'", "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
addEntity(
|
|
136
|
+
name: string,
|
|
137
|
+
entityType = "unknown",
|
|
138
|
+
properties: Record<string, unknown> | null = null,
|
|
139
|
+
): string {
|
|
140
|
+
const id = this.entityId(name);
|
|
141
|
+
this.db
|
|
142
|
+
.query(
|
|
143
|
+
"INSERT OR REPLACE INTO entities (id, name, type, properties) VALUES (?, ?, ?, ?)",
|
|
144
|
+
)
|
|
145
|
+
.run(id, name, entityType, JSON.stringify(properties ?? {}));
|
|
146
|
+
return id;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
addTriple(
|
|
150
|
+
subject: string,
|
|
151
|
+
predicate: string,
|
|
152
|
+
object: string,
|
|
153
|
+
validFrom: string | null = null,
|
|
154
|
+
validTo: string | null = null,
|
|
155
|
+
confidence = 1.0,
|
|
156
|
+
sourceCloset: string | null = null,
|
|
157
|
+
sourceFile: string | null = null,
|
|
158
|
+
): string {
|
|
159
|
+
const subjectId = this.entityId(subject);
|
|
160
|
+
const objectId = this.entityId(object);
|
|
161
|
+
const normalizedPredicate = predicate.toLowerCase().replaceAll(" ", "_");
|
|
162
|
+
|
|
163
|
+
this.db
|
|
164
|
+
.query("INSERT OR IGNORE INTO entities (id, name) VALUES (?, ?)")
|
|
165
|
+
.run(subjectId, subject);
|
|
166
|
+
this.db
|
|
167
|
+
.query("INSERT OR IGNORE INTO entities (id, name) VALUES (?, ?)")
|
|
168
|
+
.run(objectId, object);
|
|
169
|
+
|
|
170
|
+
const existing = this.db
|
|
171
|
+
.query<{ id: string }, [string, string, string]>(
|
|
172
|
+
"SELECT id FROM triples WHERE subject = ? AND predicate = ? AND object = ? AND valid_to IS NULL",
|
|
173
|
+
)
|
|
174
|
+
.get(subjectId, normalizedPredicate, objectId);
|
|
175
|
+
|
|
176
|
+
if (existing) {
|
|
177
|
+
return existing.id;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const hash = createHash("md5")
|
|
181
|
+
.update(`${validFrom ?? "null"}${new Date().toISOString()}`)
|
|
182
|
+
.digest("hex")
|
|
183
|
+
.slice(0, 8);
|
|
184
|
+
const tripleId = `t_${subjectId}_${normalizedPredicate}_${objectId}_${hash}`;
|
|
185
|
+
|
|
186
|
+
this.db
|
|
187
|
+
.query(
|
|
188
|
+
`INSERT INTO triples (
|
|
189
|
+
id, subject, predicate, object, valid_from, valid_to, confidence, source_closet, source_file
|
|
190
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
191
|
+
)
|
|
192
|
+
.run(
|
|
193
|
+
tripleId,
|
|
194
|
+
subjectId,
|
|
195
|
+
normalizedPredicate,
|
|
196
|
+
objectId,
|
|
197
|
+
validFrom,
|
|
198
|
+
validTo,
|
|
199
|
+
confidence,
|
|
200
|
+
sourceCloset,
|
|
201
|
+
sourceFile,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return tripleId;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
invalidateTriple(
|
|
208
|
+
subject: string,
|
|
209
|
+
predicate: string,
|
|
210
|
+
object: string,
|
|
211
|
+
ended: string | null = null,
|
|
212
|
+
): void {
|
|
213
|
+
const subjectId = this.entityId(subject);
|
|
214
|
+
const objectId = this.entityId(object);
|
|
215
|
+
const normalizedPredicate = predicate.toLowerCase().replaceAll(" ", "_");
|
|
216
|
+
const endedAt = ended ?? new Date().toISOString().slice(0, 10);
|
|
217
|
+
|
|
218
|
+
this.db
|
|
219
|
+
.query(
|
|
220
|
+
"UPDATE triples SET valid_to = ? WHERE subject = ? AND predicate = ? AND object = ? AND valid_to IS NULL",
|
|
221
|
+
)
|
|
222
|
+
.run(endedAt, subjectId, normalizedPredicate, objectId);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
queryEntity(
|
|
226
|
+
name: string,
|
|
227
|
+
asOf: string | null = null,
|
|
228
|
+
direction: "outgoing" | "incoming" | "both" = "outgoing",
|
|
229
|
+
): QueryResult[] {
|
|
230
|
+
const entityId = this.entityId(name);
|
|
231
|
+
const results: QueryResult[] = [];
|
|
232
|
+
|
|
233
|
+
if (direction === "outgoing" || direction === "both") {
|
|
234
|
+
let query = `
|
|
235
|
+
SELECT
|
|
236
|
+
t.predicate,
|
|
237
|
+
t.valid_from,
|
|
238
|
+
t.valid_to,
|
|
239
|
+
t.confidence,
|
|
240
|
+
t.source_closet,
|
|
241
|
+
e.name AS related_name
|
|
242
|
+
FROM triples t
|
|
243
|
+
JOIN entities e ON t.object = e.id
|
|
244
|
+
WHERE t.subject = ?
|
|
245
|
+
`;
|
|
246
|
+
const params: [string, ...string[]] = [entityId];
|
|
247
|
+
if (asOf) {
|
|
248
|
+
query += " AND (t.valid_from IS NULL OR t.valid_from <= ?) AND (t.valid_to IS NULL OR t.valid_to >= ?)";
|
|
249
|
+
params.push(asOf, asOf);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const row of this.db.query<QueryEntityRow, [string, ...string[]]>(query).all(...params)) {
|
|
253
|
+
results.push({
|
|
254
|
+
direction: "outgoing",
|
|
255
|
+
subject: name,
|
|
256
|
+
predicate: row.predicate,
|
|
257
|
+
object: row.related_name,
|
|
258
|
+
validFrom: row.valid_from,
|
|
259
|
+
validTo: row.valid_to,
|
|
260
|
+
confidence: row.confidence,
|
|
261
|
+
sourceCloset: row.source_closet,
|
|
262
|
+
current: row.valid_to === null,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (direction === "incoming" || direction === "both") {
|
|
268
|
+
let query = `
|
|
269
|
+
SELECT
|
|
270
|
+
t.predicate,
|
|
271
|
+
t.valid_from,
|
|
272
|
+
t.valid_to,
|
|
273
|
+
t.confidence,
|
|
274
|
+
t.source_closet,
|
|
275
|
+
e.name AS related_name
|
|
276
|
+
FROM triples t
|
|
277
|
+
JOIN entities e ON t.subject = e.id
|
|
278
|
+
WHERE t.object = ?
|
|
279
|
+
`;
|
|
280
|
+
const params: [string, ...string[]] = [entityId];
|
|
281
|
+
if (asOf) {
|
|
282
|
+
query += " AND (t.valid_from IS NULL OR t.valid_from <= ?) AND (t.valid_to IS NULL OR t.valid_to >= ?)";
|
|
283
|
+
params.push(asOf, asOf);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const row of this.db.query<QueryEntityRow, [string, ...string[]]>(query).all(...params)) {
|
|
287
|
+
results.push({
|
|
288
|
+
direction: "incoming",
|
|
289
|
+
subject: row.related_name,
|
|
290
|
+
predicate: row.predicate,
|
|
291
|
+
object: name,
|
|
292
|
+
validFrom: row.valid_from,
|
|
293
|
+
validTo: row.valid_to,
|
|
294
|
+
confidence: row.confidence,
|
|
295
|
+
sourceCloset: row.source_closet,
|
|
296
|
+
current: row.valid_to === null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
queryRelationship(predicate: string, asOf: string | null = null): QueryResult[] {
|
|
305
|
+
const normalizedPredicate = predicate.toLowerCase().replaceAll(" ", "_");
|
|
306
|
+
let query = `
|
|
307
|
+
SELECT
|
|
308
|
+
t.valid_from,
|
|
309
|
+
t.valid_to,
|
|
310
|
+
s.name AS sub_name,
|
|
311
|
+
o.name AS obj_name
|
|
312
|
+
FROM triples t
|
|
313
|
+
JOIN entities s ON t.subject = s.id
|
|
314
|
+
JOIN entities o ON t.object = o.id
|
|
315
|
+
WHERE t.predicate = ?
|
|
316
|
+
`;
|
|
317
|
+
const params: [string, ...string[]] = [normalizedPredicate];
|
|
318
|
+
|
|
319
|
+
if (asOf) {
|
|
320
|
+
query += " AND (t.valid_from IS NULL OR t.valid_from <= ?) AND (t.valid_to IS NULL OR t.valid_to >= ?)";
|
|
321
|
+
params.push(asOf, asOf);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return this.db
|
|
325
|
+
.query<Pick<TimelineRow, "valid_from" | "valid_to" | "sub_name" | "obj_name">, [string, ...string[]]>(query)
|
|
326
|
+
.all(...params)
|
|
327
|
+
.map((row) => ({
|
|
328
|
+
subject: row.sub_name,
|
|
329
|
+
predicate: normalizedPredicate,
|
|
330
|
+
object: row.obj_name,
|
|
331
|
+
validFrom: row.valid_from,
|
|
332
|
+
validTo: row.valid_to,
|
|
333
|
+
current: row.valid_to === null,
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getTimeline(entityName?: string | null): QueryResult[] {
|
|
338
|
+
if (entityName) {
|
|
339
|
+
const entityId = this.entityId(entityName);
|
|
340
|
+
return this.db
|
|
341
|
+
.query<TimelineRow, [string, string]>(`
|
|
342
|
+
SELECT
|
|
343
|
+
t.predicate,
|
|
344
|
+
t.valid_from,
|
|
345
|
+
t.valid_to,
|
|
346
|
+
s.name AS sub_name,
|
|
347
|
+
o.name AS obj_name
|
|
348
|
+
FROM triples t
|
|
349
|
+
JOIN entities s ON t.subject = s.id
|
|
350
|
+
JOIN entities o ON t.object = o.id
|
|
351
|
+
WHERE (t.subject = ? OR t.object = ?)
|
|
352
|
+
ORDER BY t.valid_from IS NULL, t.valid_from ASC
|
|
353
|
+
`)
|
|
354
|
+
.all(entityId, entityId)
|
|
355
|
+
.map((row) => ({
|
|
356
|
+
subject: row.sub_name,
|
|
357
|
+
predicate: row.predicate,
|
|
358
|
+
object: row.obj_name,
|
|
359
|
+
validFrom: row.valid_from,
|
|
360
|
+
validTo: row.valid_to,
|
|
361
|
+
current: row.valid_to === null,
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return this.db
|
|
366
|
+
.query<TimelineRow, []>(`
|
|
367
|
+
SELECT
|
|
368
|
+
t.predicate,
|
|
369
|
+
t.valid_from,
|
|
370
|
+
t.valid_to,
|
|
371
|
+
s.name AS sub_name,
|
|
372
|
+
o.name AS obj_name
|
|
373
|
+
FROM triples t
|
|
374
|
+
JOIN entities s ON t.subject = s.id
|
|
375
|
+
JOIN entities o ON t.object = o.id
|
|
376
|
+
ORDER BY t.valid_from IS NULL, t.valid_from ASC
|
|
377
|
+
LIMIT 100
|
|
378
|
+
`)
|
|
379
|
+
.all()
|
|
380
|
+
.map((row) => ({
|
|
381
|
+
subject: row.sub_name,
|
|
382
|
+
predicate: row.predicate,
|
|
383
|
+
object: row.obj_name,
|
|
384
|
+
validFrom: row.valid_from,
|
|
385
|
+
validTo: row.valid_to,
|
|
386
|
+
current: row.valid_to === null,
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
getStats(): StatsResult {
|
|
391
|
+
const entities = this.db.query<CountRow, []>("SELECT COUNT(*) AS count FROM entities").get()?.count ?? 0;
|
|
392
|
+
const triples = this.db.query<CountRow, []>("SELECT COUNT(*) AS count FROM triples").get()?.count ?? 0;
|
|
393
|
+
const currentFacts =
|
|
394
|
+
this.db.query<CountRow, []>("SELECT COUNT(*) AS count FROM triples WHERE valid_to IS NULL").get()?.count ?? 0;
|
|
395
|
+
const relationshipTypes = this.db
|
|
396
|
+
.query<PredicateRow, []>("SELECT DISTINCT predicate FROM triples ORDER BY predicate")
|
|
397
|
+
.all()
|
|
398
|
+
.map((row) => row.predicate);
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
entities,
|
|
402
|
+
triples,
|
|
403
|
+
currentFacts,
|
|
404
|
+
expiredFacts: triples - currentFacts,
|
|
405
|
+
relationshipTypes,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
seedFromEntityFacts(entityFacts: Record<string, EntityFact>): void {
|
|
410
|
+
for (const [key, facts] of Object.entries(entityFacts)) {
|
|
411
|
+
const name = facts.full_name ?? `${key.charAt(0).toUpperCase()}${key.slice(1)}`;
|
|
412
|
+
const entityType = facts.type ?? "person";
|
|
413
|
+
|
|
414
|
+
this.addEntity(name, entityType, {
|
|
415
|
+
gender: facts.gender ?? "",
|
|
416
|
+
birthday: facts.birthday ?? "",
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const parent = facts.parent;
|
|
420
|
+
if (parent) {
|
|
421
|
+
this.addTriple(name, "child_of", capitalize(parent), facts.birthday ?? null);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const partner = facts.partner;
|
|
425
|
+
if (partner) {
|
|
426
|
+
this.addTriple(name, "married_to", capitalize(partner));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const relationship = facts.relationship ?? "";
|
|
430
|
+
if (relationship === "daughter") {
|
|
431
|
+
this.addTriple(
|
|
432
|
+
name,
|
|
433
|
+
"is_child_of",
|
|
434
|
+
capitalize(facts.parent ?? "") || name,
|
|
435
|
+
facts.birthday ?? null,
|
|
436
|
+
);
|
|
437
|
+
} else if (relationship === "husband") {
|
|
438
|
+
this.addTriple(name, "is_partner_of", capitalize(facts.partner ?? name));
|
|
439
|
+
} else if (relationship === "brother") {
|
|
440
|
+
this.addTriple(name, "is_sibling_of", capitalize(facts.sibling ?? name));
|
|
441
|
+
} else if (relationship === "dog") {
|
|
442
|
+
this.addTriple(name, "is_pet_of", capitalize(facts.owner ?? name));
|
|
443
|
+
this.addEntity(name, "animal");
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const interest of facts.interests ?? []) {
|
|
447
|
+
this.addTriple(name, "loves", capitalize(interest), "2025-01-01");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
close(): void {
|
|
453
|
+
this.db.close();
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function capitalize(value: string): string {
|
|
458
|
+
return value ? `${value.charAt(0).toUpperCase()}${value.slice(1)}` : value;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export default KnowledgeGraph;
|