@lotargo/memory_plugin 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 +243 -234
- package/mcp-server/admin/server.js +228 -228
- package/mcp-server/admin/snapshot.js +303 -303
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -193
- package/mcp-server/benchmarks/test_dual_layer.js +140 -140
- package/mcp-server/cli.js +293 -7
- package/mcp-server/config/config_manager.js +11 -7
- package/mcp-server/db/database.js +43 -43
- package/mcp-server/graph/graph_extractor.js +72 -72
- package/mcp-server/graph/knowledge_linker.js +102 -102
- package/mcp-server/index.js +454 -454
- package/mcp-server/ingest/chunker.js +337 -337
- package/mcp-server/ingest/exporter.js +80 -80
- package/mcp-server/ingest/normalizer.js +104 -104
- package/mcp-server/ingest/pipeline.js +22 -7
- package/mcp-server/ingest/sentence_segmenter.js +74 -74
- package/mcp-server/memory.js +72 -72
- package/mcp-server/ml/gpu_monitor.js +166 -0
- package/mcp-server/ml/model_manager.js +327 -17
- package/mcp-server/preinstall.js +44 -22
- package/mcp-server/retrieval/retriever.js +10 -5
- package/mcp-server/setup.js +148 -148
- package/mcp-server/storage/blob_store.js +62 -62
- package/opencode-plugin/index.js +244 -244
- package/package.json +58 -54
- package/skills/using-memory/SKILL.md +122 -122
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import { getDatabase } from "../db/database.js";
|
|
2
|
-
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { MEMORY_DIR } from "../memory.js";
|
|
5
|
-
|
|
6
|
-
export const EXPORTS_DIR = join(MEMORY_DIR, "exports");
|
|
7
|
-
|
|
8
|
-
export function ensureExportsDir() {
|
|
9
|
-
if (!existsSync(EXPORTS_DIR)) {
|
|
10
|
-
mkdirSync(EXPORTS_DIR, { recursive: true });
|
|
11
|
-
}
|
|
12
|
-
return EXPORTS_DIR;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function exportDocumentData(docIdOrPath, customDb = null) {
|
|
16
|
-
const db = customDb || getDatabase();
|
|
17
|
-
const doc = db.prepare("SELECT * FROM documents WHERE id = ? OR path = ?").get(docIdOrPath, docIdOrPath);
|
|
18
|
-
if (!doc) {
|
|
19
|
-
throw new Error(`Document not found for ID or path: ${docIdOrPath}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let toc = null;
|
|
23
|
-
try {
|
|
24
|
-
toc = doc.toc_json ? JSON.parse(doc.toc_json) : null;
|
|
25
|
-
} catch {
|
|
26
|
-
toc = doc.toc_json;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let metadata = null;
|
|
30
|
-
try {
|
|
31
|
-
metadata = doc.metadata_json ? JSON.parse(doc.metadata_json) : null;
|
|
32
|
-
} catch {
|
|
33
|
-
metadata = doc.metadata_json;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const sections = db.prepare("SELECT id, heading, breadcrumbs, content, token_count FROM sections WHERE doc_id = ?").all(doc.id);
|
|
37
|
-
const mediumChunks = db.prepare("SELECT id, section_id, content, block_type, token_count, created_at FROM medium_chunks WHERE doc_id = ?").all(doc.id);
|
|
38
|
-
const microChunks = db.prepare("SELECT id, medium_id, section_id, content, token_count FROM micro_chunks WHERE doc_id = ?").all(doc.id);
|
|
39
|
-
const graphEdges = db.prepare("SELECT source_id, target_id, relation_type, metadata_json FROM graph_edges WHERE source_id = ? OR target_id = ?").all(doc.id, doc.id);
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
document: {
|
|
43
|
-
id: doc.id,
|
|
44
|
-
path: doc.path,
|
|
45
|
-
title: doc.title,
|
|
46
|
-
blob_hash: doc.blob_hash,
|
|
47
|
-
checksum: doc.checksum,
|
|
48
|
-
toc,
|
|
49
|
-
metadata,
|
|
50
|
-
created_at: doc.created_at,
|
|
51
|
-
updated_at: doc.updated_at,
|
|
52
|
-
},
|
|
53
|
-
counts: {
|
|
54
|
-
sections: sections.length,
|
|
55
|
-
medium_chunks: mediumChunks.length,
|
|
56
|
-
micro_chunks: microChunks.length,
|
|
57
|
-
graph_edges: graphEdges.length,
|
|
58
|
-
},
|
|
59
|
-
sections,
|
|
60
|
-
medium_chunks: mediumChunks,
|
|
61
|
-
micro_chunks: microChunks,
|
|
62
|
-
graph_edges: graphEdges,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function exportDocumentToJsonString(docIdOrPath, customDb = null) {
|
|
67
|
-
const data = exportDocumentData(docIdOrPath, customDb);
|
|
68
|
-
return JSON.stringify(data, null, 2);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function exportDocumentToFile(docIdOrPath, outputPath = null, customDb = null) {
|
|
72
|
-
const targetDir = ensureExportsDir();
|
|
73
|
-
const db = customDb || getDatabase();
|
|
74
|
-
const data = exportDocumentData(docIdOrPath, db);
|
|
75
|
-
const jsonStr = JSON.stringify(data, null, 2);
|
|
76
|
-
|
|
77
|
-
const finalPath = outputPath || join(targetDir, `doc_export_${data.document.id}.json`);
|
|
78
|
-
writeFileSync(finalPath, jsonStr, "utf-8");
|
|
79
|
-
return finalPath;
|
|
80
|
-
}
|
|
1
|
+
import { getDatabase } from "../db/database.js";
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { MEMORY_DIR } from "../memory.js";
|
|
5
|
+
|
|
6
|
+
export const EXPORTS_DIR = join(MEMORY_DIR, "exports");
|
|
7
|
+
|
|
8
|
+
export function ensureExportsDir() {
|
|
9
|
+
if (!existsSync(EXPORTS_DIR)) {
|
|
10
|
+
mkdirSync(EXPORTS_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
return EXPORTS_DIR;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function exportDocumentData(docIdOrPath, customDb = null) {
|
|
16
|
+
const db = customDb || getDatabase();
|
|
17
|
+
const doc = db.prepare("SELECT * FROM documents WHERE id = ? OR path = ?").get(docIdOrPath, docIdOrPath);
|
|
18
|
+
if (!doc) {
|
|
19
|
+
throw new Error(`Document not found for ID or path: ${docIdOrPath}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let toc = null;
|
|
23
|
+
try {
|
|
24
|
+
toc = doc.toc_json ? JSON.parse(doc.toc_json) : null;
|
|
25
|
+
} catch {
|
|
26
|
+
toc = doc.toc_json;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let metadata = null;
|
|
30
|
+
try {
|
|
31
|
+
metadata = doc.metadata_json ? JSON.parse(doc.metadata_json) : null;
|
|
32
|
+
} catch {
|
|
33
|
+
metadata = doc.metadata_json;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sections = db.prepare("SELECT id, heading, breadcrumbs, content, token_count FROM sections WHERE doc_id = ?").all(doc.id);
|
|
37
|
+
const mediumChunks = db.prepare("SELECT id, section_id, content, block_type, token_count, created_at FROM medium_chunks WHERE doc_id = ?").all(doc.id);
|
|
38
|
+
const microChunks = db.prepare("SELECT id, medium_id, section_id, content, token_count FROM micro_chunks WHERE doc_id = ?").all(doc.id);
|
|
39
|
+
const graphEdges = db.prepare("SELECT source_id, target_id, relation_type, metadata_json FROM graph_edges WHERE source_id = ? OR target_id = ?").all(doc.id, doc.id);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
document: {
|
|
43
|
+
id: doc.id,
|
|
44
|
+
path: doc.path,
|
|
45
|
+
title: doc.title,
|
|
46
|
+
blob_hash: doc.blob_hash,
|
|
47
|
+
checksum: doc.checksum,
|
|
48
|
+
toc,
|
|
49
|
+
metadata,
|
|
50
|
+
created_at: doc.created_at,
|
|
51
|
+
updated_at: doc.updated_at,
|
|
52
|
+
},
|
|
53
|
+
counts: {
|
|
54
|
+
sections: sections.length,
|
|
55
|
+
medium_chunks: mediumChunks.length,
|
|
56
|
+
micro_chunks: microChunks.length,
|
|
57
|
+
graph_edges: graphEdges.length,
|
|
58
|
+
},
|
|
59
|
+
sections,
|
|
60
|
+
medium_chunks: mediumChunks,
|
|
61
|
+
micro_chunks: microChunks,
|
|
62
|
+
graph_edges: graphEdges,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function exportDocumentToJsonString(docIdOrPath, customDb = null) {
|
|
67
|
+
const data = exportDocumentData(docIdOrPath, customDb);
|
|
68
|
+
return JSON.stringify(data, null, 2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function exportDocumentToFile(docIdOrPath, outputPath = null, customDb = null) {
|
|
72
|
+
const targetDir = ensureExportsDir();
|
|
73
|
+
const db = customDb || getDatabase();
|
|
74
|
+
const data = exportDocumentData(docIdOrPath, db);
|
|
75
|
+
const jsonStr = JSON.stringify(data, null, 2);
|
|
76
|
+
|
|
77
|
+
const finalPath = outputPath || join(targetDir, `doc_export_${data.document.id}.json`);
|
|
78
|
+
writeFileSync(finalPath, jsonStr, "utf-8");
|
|
79
|
+
return finalPath;
|
|
80
|
+
}
|
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
import { basename, extname } from "node:path";
|
|
2
|
-
|
|
3
|
-
export function cleanHtml(html) {
|
|
4
|
-
if (!html) return "";
|
|
5
|
-
|
|
6
|
-
let cleaned = html.replace(/<(script|style|nav|header|footer|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
7
|
-
|
|
8
|
-
cleaned = cleaned.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n");
|
|
9
|
-
cleaned = cleaned.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n");
|
|
10
|
-
cleaned = cleaned.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n");
|
|
11
|
-
cleaned = cleaned.replace(/<h[4-6][^>]*>([\s\S]*?)<\/h[4-6]>/gi, "\n#### $1\n");
|
|
12
|
-
|
|
13
|
-
cleaned = cleaned.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n- $1");
|
|
14
|
-
cleaned = cleaned.replace(/<(p|div|br)[^>]*>/gi, "\n");
|
|
15
|
-
|
|
16
|
-
cleaned = cleaned.replace(/<[^>]+>/g, "");
|
|
17
|
-
|
|
18
|
-
cleaned = cleaned
|
|
19
|
-
.replace(/ /g, " ")
|
|
20
|
-
.replace(/&/g, "&")
|
|
21
|
-
.replace(/</g, "<")
|
|
22
|
-
.replace(/>/g, ">")
|
|
23
|
-
.replace(/"/g, '"')
|
|
24
|
-
.replace(/'/g, "'");
|
|
25
|
-
|
|
26
|
-
cleaned = cleaned.replace(/\n\s*\n\s*\n/g, "\n\n").trim();
|
|
27
|
-
return cleaned;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function extractTitle(markdown, fallbackName = "Untitled Document") {
|
|
31
|
-
const h1Match = markdown.match(/^#\s+(.+)$/m);
|
|
32
|
-
if (h1Match && h1Match[1].trim()) {
|
|
33
|
-
return h1Match[1].trim();
|
|
34
|
-
}
|
|
35
|
-
return fallbackName;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function stripMarkdownBadgesAndNoise(text) {
|
|
39
|
-
if (!text) return "";
|
|
40
|
-
let cleaned = text;
|
|
41
|
-
|
|
42
|
-
// 1. Remove markdown link-wrapped badges: [](link_url)
|
|
43
|
-
cleaned = cleaned.replace(/\[\s*!\[[^\]]*\]\([^)]+\)\s*\]\([^)]+\)/g, "");
|
|
44
|
-
|
|
45
|
-
// 2. Remove standalone markdown image badges:  or badge URLs
|
|
46
|
-
cleaned = cleaned.replace(/!\[[^\]]*\]\([^)]*(?:shields\.io|badge|actions\/workflows|codecov|travis-ci)[^)]*\)/gi, "");
|
|
47
|
-
|
|
48
|
-
// 3. Remove raw HTML img badge tags
|
|
49
|
-
cleaned = cleaned.replace(/<img[^>]*(?:shields\.io|badge|workflows|badge\.svg)[^>]*>/gi, "");
|
|
50
|
-
|
|
51
|
-
// 4. Remove empty HTML anchor containers often surrounding badges
|
|
52
|
-
cleaned = cleaned.replace(/<a[^>]*>\s*<\/a>/gi, "");
|
|
53
|
-
|
|
54
|
-
// 5. Normalize excessive blank lines
|
|
55
|
-
cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
|
|
56
|
-
return cleaned;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function normalizeContent({ content, type = "text", path = null, title = null }) {
|
|
60
|
-
let markdown = "";
|
|
61
|
-
let docTitle = title;
|
|
62
|
-
const fileName = path ? basename(path) : "document";
|
|
63
|
-
|
|
64
|
-
if (type === "url" || (typeof content === "string" && /<html|<body|<div/i.test(content))) {
|
|
65
|
-
markdown = cleanHtml(content);
|
|
66
|
-
docTitle = title || extractTitle(markdown, fileName);
|
|
67
|
-
} else if (type === "file" && path) {
|
|
68
|
-
const ext = extname(path).toLowerCase();
|
|
69
|
-
const codeLangs = {
|
|
70
|
-
".js": "javascript",
|
|
71
|
-
".ts": "typescript",
|
|
72
|
-
".py": "python",
|
|
73
|
-
".json": "json",
|
|
74
|
-
".yaml": "yaml",
|
|
75
|
-
".yml": "yaml",
|
|
76
|
-
".css": "css",
|
|
77
|
-
".html": "html",
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
if (codeLangs[ext]) {
|
|
81
|
-
markdown = `# ${fileName}\n\n\`\`\`${codeLangs[ext]}\n${content.trim()}\n\`\`\``;
|
|
82
|
-
docTitle = title || fileName;
|
|
83
|
-
} else {
|
|
84
|
-
markdown = content.trim();
|
|
85
|
-
docTitle = title || extractTitle(markdown, fileName);
|
|
86
|
-
}
|
|
87
|
-
} else {
|
|
88
|
-
markdown = typeof content === "string" ? content.trim() : String(content);
|
|
89
|
-
docTitle = title || extractTitle(markdown, "Direct Note");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Apply noise and badge stripping to all developer documentation
|
|
93
|
-
markdown = stripMarkdownBadgesAndNoise(markdown);
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
markdown,
|
|
97
|
-
title: docTitle,
|
|
98
|
-
metadata: {
|
|
99
|
-
source_type: type,
|
|
100
|
-
original_path: path || null,
|
|
101
|
-
char_count: markdown.length,
|
|
102
|
-
},
|
|
103
|
-
};
|
|
104
|
-
}
|
|
1
|
+
import { basename, extname } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function cleanHtml(html) {
|
|
4
|
+
if (!html) return "";
|
|
5
|
+
|
|
6
|
+
let cleaned = html.replace(/<(script|style|nav|header|footer|svg|noscript)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
7
|
+
|
|
8
|
+
cleaned = cleaned.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n");
|
|
9
|
+
cleaned = cleaned.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n");
|
|
10
|
+
cleaned = cleaned.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n");
|
|
11
|
+
cleaned = cleaned.replace(/<h[4-6][^>]*>([\s\S]*?)<\/h[4-6]>/gi, "\n#### $1\n");
|
|
12
|
+
|
|
13
|
+
cleaned = cleaned.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "\n- $1");
|
|
14
|
+
cleaned = cleaned.replace(/<(p|div|br)[^>]*>/gi, "\n");
|
|
15
|
+
|
|
16
|
+
cleaned = cleaned.replace(/<[^>]+>/g, "");
|
|
17
|
+
|
|
18
|
+
cleaned = cleaned
|
|
19
|
+
.replace(/ /g, " ")
|
|
20
|
+
.replace(/&/g, "&")
|
|
21
|
+
.replace(/</g, "<")
|
|
22
|
+
.replace(/>/g, ">")
|
|
23
|
+
.replace(/"/g, '"')
|
|
24
|
+
.replace(/'/g, "'");
|
|
25
|
+
|
|
26
|
+
cleaned = cleaned.replace(/\n\s*\n\s*\n/g, "\n\n").trim();
|
|
27
|
+
return cleaned;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function extractTitle(markdown, fallbackName = "Untitled Document") {
|
|
31
|
+
const h1Match = markdown.match(/^#\s+(.+)$/m);
|
|
32
|
+
if (h1Match && h1Match[1].trim()) {
|
|
33
|
+
return h1Match[1].trim();
|
|
34
|
+
}
|
|
35
|
+
return fallbackName;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function stripMarkdownBadgesAndNoise(text) {
|
|
39
|
+
if (!text) return "";
|
|
40
|
+
let cleaned = text;
|
|
41
|
+
|
|
42
|
+
// 1. Remove markdown link-wrapped badges: [](link_url)
|
|
43
|
+
cleaned = cleaned.replace(/\[\s*!\[[^\]]*\]\([^)]+\)\s*\]\([^)]+\)/g, "");
|
|
44
|
+
|
|
45
|
+
// 2. Remove standalone markdown image badges:  or badge URLs
|
|
46
|
+
cleaned = cleaned.replace(/!\[[^\]]*\]\([^)]*(?:shields\.io|badge|actions\/workflows|codecov|travis-ci)[^)]*\)/gi, "");
|
|
47
|
+
|
|
48
|
+
// 3. Remove raw HTML img badge tags
|
|
49
|
+
cleaned = cleaned.replace(/<img[^>]*(?:shields\.io|badge|workflows|badge\.svg)[^>]*>/gi, "");
|
|
50
|
+
|
|
51
|
+
// 4. Remove empty HTML anchor containers often surrounding badges
|
|
52
|
+
cleaned = cleaned.replace(/<a[^>]*>\s*<\/a>/gi, "");
|
|
53
|
+
|
|
54
|
+
// 5. Normalize excessive blank lines
|
|
55
|
+
cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, "\n\n").trim();
|
|
56
|
+
return cleaned;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeContent({ content, type = "text", path = null, title = null }) {
|
|
60
|
+
let markdown = "";
|
|
61
|
+
let docTitle = title;
|
|
62
|
+
const fileName = path ? basename(path) : "document";
|
|
63
|
+
|
|
64
|
+
if (type === "url" || (typeof content === "string" && /<html|<body|<div/i.test(content))) {
|
|
65
|
+
markdown = cleanHtml(content);
|
|
66
|
+
docTitle = title || extractTitle(markdown, fileName);
|
|
67
|
+
} else if (type === "file" && path) {
|
|
68
|
+
const ext = extname(path).toLowerCase();
|
|
69
|
+
const codeLangs = {
|
|
70
|
+
".js": "javascript",
|
|
71
|
+
".ts": "typescript",
|
|
72
|
+
".py": "python",
|
|
73
|
+
".json": "json",
|
|
74
|
+
".yaml": "yaml",
|
|
75
|
+
".yml": "yaml",
|
|
76
|
+
".css": "css",
|
|
77
|
+
".html": "html",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (codeLangs[ext]) {
|
|
81
|
+
markdown = `# ${fileName}\n\n\`\`\`${codeLangs[ext]}\n${content.trim()}\n\`\`\``;
|
|
82
|
+
docTitle = title || fileName;
|
|
83
|
+
} else {
|
|
84
|
+
markdown = content.trim();
|
|
85
|
+
docTitle = title || extractTitle(markdown, fileName);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
markdown = typeof content === "string" ? content.trim() : String(content);
|
|
89
|
+
docTitle = title || extractTitle(markdown, "Direct Note");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply noise and badge stripping to all developer documentation
|
|
93
|
+
markdown = stripMarkdownBadgesAndNoise(markdown);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
markdown,
|
|
97
|
+
title: docTitle,
|
|
98
|
+
metadata: {
|
|
99
|
+
source_type: type,
|
|
100
|
+
original_path: path || null,
|
|
101
|
+
char_count: markdown.length,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -3,8 +3,9 @@ import { getDatabase, BLOBS_DIR } from "../db/database.js";
|
|
|
3
3
|
import { saveBlob, deleteBlob } from "../storage/blob_store.js";
|
|
4
4
|
import { normalizeContent } from "./normalizer.js";
|
|
5
5
|
import { buildTripleHierarchy } from "./chunker.js";
|
|
6
|
-
import { embedText, vectorToBuffer } from "../ml/model_manager.js";
|
|
6
|
+
import { embedText, embedBatch, vectorToBuffer } from "../ml/model_manager.js";
|
|
7
7
|
import { buildGraphEdges, saveGraphEdges } from "../graph/graph_extractor.js";
|
|
8
|
+
import { getConfig } from "../config/config_manager.js";
|
|
8
9
|
|
|
9
10
|
export async function ingestDocument({
|
|
10
11
|
content,
|
|
@@ -28,13 +29,27 @@ export async function ingestDocument({
|
|
|
28
29
|
|
|
29
30
|
const hierarchy = buildTripleHierarchy(markdown, docId, docTitle);
|
|
30
31
|
|
|
31
|
-
if (generateEmbeddings) {
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
if (generateEmbeddings && hierarchy.microChunks.length > 0) {
|
|
33
|
+
const BATCH_SIZE = getConfig().batchSize || 12;
|
|
34
|
+
|
|
35
|
+
// Smart Batching: Sort micro-chunks by character/token length to minimize ONNX zero-padding overhead
|
|
36
|
+
const indexedItems = hierarchy.microChunks.map((micro, idx) => ({
|
|
37
|
+
index: idx,
|
|
38
|
+
text: micro.breadcrumbs
|
|
34
39
|
? `${micro.content}\n\nContext: ${docTitle} > ${micro.breadcrumbs}`
|
|
35
|
-
: `${micro.content}\n\nContext: ${docTitle}
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
: `${micro.content}\n\nContext: ${docTitle}`,
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
indexedItems.sort((a, b) => a.text.length - b.text.length);
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < indexedItems.length; i += BATCH_SIZE) {
|
|
46
|
+
const batch = indexedItems.slice(i, i + BATCH_SIZE);
|
|
47
|
+
const batchTexts = batch.map((item) => item.text);
|
|
48
|
+
const batchVecs = await embedBatch(batchTexts, false);
|
|
49
|
+
for (let j = 0; j < batchVecs.length; j++) {
|
|
50
|
+
const origIdx = batch[j].index;
|
|
51
|
+
hierarchy.microChunks[origIdx].vector = vectorToBuffer(batchVecs[j]);
|
|
52
|
+
}
|
|
38
53
|
}
|
|
39
54
|
} else {
|
|
40
55
|
for (const micro of hierarchy.microChunks) {
|
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
// Multilingual Sentence Segmenter using Intl.Segmenter with Abbreviation & Boundary Protection
|
|
2
|
-
|
|
3
|
-
const RU_ABBREVIATIONS = new Set([
|
|
4
|
-
"т.д", "т.п", "т.е", "и др", "г", "гг", "ул", "руб", "коп", "стр", "рис", "см", "им", "пер", "д", "к", "п", "в"
|
|
5
|
-
]);
|
|
6
|
-
|
|
7
|
-
const EN_ABBREVIATIONS = new Set([
|
|
8
|
-
"e.g", "i.e", "etc", "vs", "dr", "mr", "mrs", "ms", "prof", "inc", "ltd", "co", "vol", "no", "p", "pp", "fig"
|
|
9
|
-
]);
|
|
10
|
-
|
|
11
|
-
function isAbbreviation(word) {
|
|
12
|
-
if (!word) return false;
|
|
13
|
-
const clean = word.trim().toLowerCase().replace(/\.$/, "");
|
|
14
|
-
return RU_ABBREVIATIONS.has(clean) || EN_ABBREVIATIONS.has(clean);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function splitSentencesMultilingual(text, langHint = "ru") {
|
|
18
|
-
if (!text || typeof text !== "string") return [];
|
|
19
|
-
const trimmed = text.trim();
|
|
20
|
-
if (trimmed.length === 0) return [];
|
|
21
|
-
|
|
22
|
-
let rawSegments = [];
|
|
23
|
-
|
|
24
|
-
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
25
|
-
try {
|
|
26
|
-
const segmenter = new Intl.Segmenter([langHint, "ru", "en"], { granularity: "sentence" });
|
|
27
|
-
const iter = segmenter.segment(trimmed);
|
|
28
|
-
for (const seg of iter) {
|
|
29
|
-
if (seg.segment.trim().length > 0) {
|
|
30
|
-
rawSegments.push(seg.segment);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
} catch {
|
|
34
|
-
rawSegments = fallbackSentenceSplit(trimmed);
|
|
35
|
-
}
|
|
36
|
-
} else {
|
|
37
|
-
rawSegments = fallbackSentenceSplit(trimmed);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Post-process & merge false splits caused by abbreviations or numbers (e.g., "v1.0", "e.g.", "т. д.")
|
|
41
|
-
const sentences = [];
|
|
42
|
-
let current = "";
|
|
43
|
-
|
|
44
|
-
for (let i = 0; i < rawSegments.length; i++) {
|
|
45
|
-
const seg = rawSegments[i];
|
|
46
|
-
if (!current) {
|
|
47
|
-
current = seg;
|
|
48
|
-
} else {
|
|
49
|
-
const lastWordMatch = current.trim().match(/([a-zA-Zа-яА-Я0-9._-]+)\s*\.?$/);
|
|
50
|
-
const lastWord = lastWordMatch ? lastWordMatch[1] : "";
|
|
51
|
-
|
|
52
|
-
const isNumDot = /\b\d+\.$/.test(current.trim());
|
|
53
|
-
const isAbbr = isAbbreviation(lastWord) || isNumDot;
|
|
54
|
-
const startsWithLowercase = /^[a-zа-я]/.test(seg.trim());
|
|
55
|
-
|
|
56
|
-
if (isAbbr || startsWithLowercase) {
|
|
57
|
-
current += seg;
|
|
58
|
-
} else {
|
|
59
|
-
sentences.push(current.trim());
|
|
60
|
-
current = seg;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (current && current.trim().length > 0) {
|
|
66
|
-
sentences.push(current.trim());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return sentences;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function fallbackSentenceSplit(text) {
|
|
73
|
-
return text.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 0);
|
|
74
|
-
}
|
|
1
|
+
// Multilingual Sentence Segmenter using Intl.Segmenter with Abbreviation & Boundary Protection
|
|
2
|
+
|
|
3
|
+
const RU_ABBREVIATIONS = new Set([
|
|
4
|
+
"т.д", "т.п", "т.е", "и др", "г", "гг", "ул", "руб", "коп", "стр", "рис", "см", "им", "пер", "д", "к", "п", "в"
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
const EN_ABBREVIATIONS = new Set([
|
|
8
|
+
"e.g", "i.e", "etc", "vs", "dr", "mr", "mrs", "ms", "prof", "inc", "ltd", "co", "vol", "no", "p", "pp", "fig"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function isAbbreviation(word) {
|
|
12
|
+
if (!word) return false;
|
|
13
|
+
const clean = word.trim().toLowerCase().replace(/\.$/, "");
|
|
14
|
+
return RU_ABBREVIATIONS.has(clean) || EN_ABBREVIATIONS.has(clean);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function splitSentencesMultilingual(text, langHint = "ru") {
|
|
18
|
+
if (!text || typeof text !== "string") return [];
|
|
19
|
+
const trimmed = text.trim();
|
|
20
|
+
if (trimmed.length === 0) return [];
|
|
21
|
+
|
|
22
|
+
let rawSegments = [];
|
|
23
|
+
|
|
24
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
25
|
+
try {
|
|
26
|
+
const segmenter = new Intl.Segmenter([langHint, "ru", "en"], { granularity: "sentence" });
|
|
27
|
+
const iter = segmenter.segment(trimmed);
|
|
28
|
+
for (const seg of iter) {
|
|
29
|
+
if (seg.segment.trim().length > 0) {
|
|
30
|
+
rawSegments.push(seg.segment);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
rawSegments = fallbackSentenceSplit(trimmed);
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
rawSegments = fallbackSentenceSplit(trimmed);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Post-process & merge false splits caused by abbreviations or numbers (e.g., "v1.0", "e.g.", "т. д.")
|
|
41
|
+
const sentences = [];
|
|
42
|
+
let current = "";
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < rawSegments.length; i++) {
|
|
45
|
+
const seg = rawSegments[i];
|
|
46
|
+
if (!current) {
|
|
47
|
+
current = seg;
|
|
48
|
+
} else {
|
|
49
|
+
const lastWordMatch = current.trim().match(/([a-zA-Zа-яА-Я0-9._-]+)\s*\.?$/);
|
|
50
|
+
const lastWord = lastWordMatch ? lastWordMatch[1] : "";
|
|
51
|
+
|
|
52
|
+
const isNumDot = /\b\d+\.$/.test(current.trim());
|
|
53
|
+
const isAbbr = isAbbreviation(lastWord) || isNumDot;
|
|
54
|
+
const startsWithLowercase = /^[a-zа-я]/.test(seg.trim());
|
|
55
|
+
|
|
56
|
+
if (isAbbr || startsWithLowercase) {
|
|
57
|
+
current += seg;
|
|
58
|
+
} else {
|
|
59
|
+
sentences.push(current.trim());
|
|
60
|
+
current = seg;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (current && current.trim().length > 0) {
|
|
66
|
+
sentences.push(current.trim());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return sentences;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fallbackSentenceSplit(text) {
|
|
73
|
+
return text.split(/(?<=[.!?])\s+/).filter((s) => s.trim().length > 0);
|
|
74
|
+
}
|