@o-lang/semantic-doc-search 1.0.32 → 1.0.34
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/package.json +1 -1
- package/src/embeddings/local.js +51 -18
- package/src/resolver.js +26 -12
- package/test-embed.js +10 -0
package/package.json
CHANGED
package/src/embeddings/local.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* - No zero vectors
|
|
8
8
|
* - Deterministic behavior
|
|
9
9
|
* - DEFENSIVE against method detaching & invalid vectors
|
|
10
|
+
* - WINDOWS-SAFE (disables SIMD, threads, proxy)
|
|
11
|
+
* - TENSOR-SAFE (handles Float32Array, Array, and all ONNX tensor types)
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
class LocalEmbedding {
|
|
@@ -29,13 +31,21 @@ class LocalEmbedding {
|
|
|
29
31
|
|
|
30
32
|
if (!this.loading) {
|
|
31
33
|
this.loading = (async () => {
|
|
32
|
-
|
|
34
|
+
// ⚠️ CRITICAL: Configure environment BEFORE loading model
|
|
35
|
+
const { env } = await import("@xenova/transformers");
|
|
33
36
|
|
|
34
|
-
// Safe
|
|
37
|
+
// Safe settings for all platforms (harmless on macOS/Linux, essential on Windows)
|
|
38
|
+
env.backends.onnx.wasm.simd = false; // Avoids AVX/SIMD crashes on older CPUs
|
|
39
|
+
env.backends.onnx.wasm.threads = false; // Prevents threading issues in Node
|
|
40
|
+
env.backends.onnx.wasm.proxy = false; // Avoids proxy complications
|
|
35
41
|
env.allowLocalModels = true;
|
|
36
42
|
env.backends.onnx.warmup = false;
|
|
43
|
+
env.cacheDir = "./.cache/embeddings"; // Explicit, project-local cache
|
|
37
44
|
|
|
38
45
|
console.log("🔄 Loading local embedding model (first run only)...");
|
|
46
|
+
console.log("⚙️ Using WASM (SIMD disabled) for cross-platform compatibility");
|
|
47
|
+
|
|
48
|
+
const { pipeline } = await import("@xenova/transformers");
|
|
39
49
|
|
|
40
50
|
const model = await pipeline(
|
|
41
51
|
"feature-extraction",
|
|
@@ -57,9 +67,6 @@ class LocalEmbedding {
|
|
|
57
67
|
|
|
58
68
|
/* ---------------- PUBLIC API ---------------- */
|
|
59
69
|
|
|
60
|
-
/**
|
|
61
|
-
* Generate embedding for a single string
|
|
62
|
-
*/
|
|
63
70
|
async embed(text) {
|
|
64
71
|
if (typeof text !== "string" || !text.trim()) {
|
|
65
72
|
throw new Error("Embedding input must be a non-empty string");
|
|
@@ -73,11 +80,45 @@ class LocalEmbedding {
|
|
|
73
80
|
normalize: true,
|
|
74
81
|
});
|
|
75
82
|
|
|
76
|
-
//
|
|
77
|
-
|
|
83
|
+
// 🔍 DEBUG: Inspect output structure
|
|
84
|
+
console.log("🔍 Model output type:", typeof output);
|
|
85
|
+
if (output && typeof output === 'object') {
|
|
86
|
+
console.log("🔍 Output keys:", Object.keys(output));
|
|
87
|
+
console.log("🔍 Output dims:", output.dims);
|
|
88
|
+
console.log("🔍 output.data type:", Object.prototype.toString.call(output.data));
|
|
89
|
+
console.log("🔍 Is TypedArray?", ArrayBuffer.isView(output.data));
|
|
90
|
+
}
|
|
78
91
|
|
|
92
|
+
// ✅ UNIVERSAL EXTRACTION: handles Float32Array, Array, and all tensor forms
|
|
93
|
+
let vector = null;
|
|
94
|
+
|
|
95
|
+
if (output && output.data !== undefined) {
|
|
96
|
+
// Handle Float32Array, Uint8Array, etc. (standard in ONNX/WASM)
|
|
97
|
+
if (ArrayBuffer.isView(output.data)) {
|
|
98
|
+
vector = Array.from(output.data);
|
|
99
|
+
}
|
|
100
|
+
// Handle plain JS array (older backends or CPU mode)
|
|
101
|
+
else if (Array.isArray(output.data)) {
|
|
102
|
+
vector = Array.from(output.data);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Handle batch output: [tensor]
|
|
106
|
+
else if (Array.isArray(output) && output[0]?.data !== undefined) {
|
|
107
|
+
if (ArrayBuffer.isView(output[0].data)) {
|
|
108
|
+
vector = Array.from(output[0].data);
|
|
109
|
+
} else if (Array.isArray(output[0].data)) {
|
|
110
|
+
vector = Array.from(output[0].data);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Fallback: raw array (rare)
|
|
114
|
+
else if (Array.isArray(output)) {
|
|
115
|
+
vector = output;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Final validation
|
|
79
119
|
if (!Array.isArray(vector) || vector.length !== this.dim) {
|
|
80
|
-
console.error("❌ Invalid embedding vector
|
|
120
|
+
console.error("❌ Invalid embedding vector length:", vector?.length);
|
|
121
|
+
console.error("❌ First few values:", vector?.slice?.(0, 5));
|
|
81
122
|
throw new Error(`Invalid embedding dimension: ${vector?.length || 0} (expected ${this.dim})`);
|
|
82
123
|
}
|
|
83
124
|
|
|
@@ -85,15 +126,12 @@ class LocalEmbedding {
|
|
|
85
126
|
} catch (err) {
|
|
86
127
|
console.error(
|
|
87
128
|
`❌ Embedding failed for text: "${text.slice(0, 60)}..."`,
|
|
88
|
-
err
|
|
129
|
+
err.message
|
|
89
130
|
);
|
|
90
131
|
throw err;
|
|
91
132
|
}
|
|
92
133
|
}
|
|
93
134
|
|
|
94
|
-
/**
|
|
95
|
-
* Batch embedding (sequential, safe)
|
|
96
|
-
*/
|
|
97
135
|
async embedBatch(texts = []) {
|
|
98
136
|
if (!Array.isArray(texts)) {
|
|
99
137
|
throw new Error("embedBatch expects an array of strings");
|
|
@@ -106,15 +144,10 @@ class LocalEmbedding {
|
|
|
106
144
|
return results;
|
|
107
145
|
}
|
|
108
146
|
|
|
109
|
-
/**
|
|
110
|
-
* Return embedding dimension
|
|
111
|
-
*/
|
|
112
147
|
getDimension() {
|
|
113
148
|
return this.dim;
|
|
114
149
|
}
|
|
115
150
|
}
|
|
116
151
|
|
|
117
|
-
/* ---------------- SINGLETON EXPORT ---------------- */
|
|
118
|
-
|
|
119
152
|
const embedder = new LocalEmbedding();
|
|
120
|
-
module.exports = embedder;
|
|
153
|
+
module.exports = embedder;
|
package/src/resolver.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const VectorRouter = require("./adapters/vectorRouter");
|
|
2
|
-
const embedder = require("./embeddings/local"); //
|
|
2
|
+
const embedder = require("./embeddings/local"); // singleton embedder
|
|
3
3
|
const { extractQuery } = require("./utils/extractQuery");
|
|
4
4
|
const { formatResults } = require("./utils/formatResults");
|
|
5
5
|
const fs = require("fs");
|
|
@@ -8,7 +8,7 @@ const crypto = require("crypto");
|
|
|
8
8
|
|
|
9
9
|
const CACHE_PATH = path.join(process.cwd(), "embeddings.json");
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Load cache for ingestion guard
|
|
12
12
|
function loadCache() {
|
|
13
13
|
try {
|
|
14
14
|
if (fs.existsSync(CACHE_PATH)) {
|
|
@@ -17,24 +17,36 @@ function loadCache() {
|
|
|
17
17
|
} catch {}
|
|
18
18
|
return {};
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
function saveCache(cache) {
|
|
21
22
|
try {
|
|
22
23
|
fs.writeFileSync(CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
23
24
|
} catch {}
|
|
24
25
|
}
|
|
26
|
+
|
|
25
27
|
function hashContent(str) {
|
|
26
28
|
return crypto.createHash("sha256").update(str).digest("hex");
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Clean text for embedding (defensive)
|
|
33
|
+
*/
|
|
34
|
+
function sanitizeTextForEmbedding(text) {
|
|
35
|
+
if (typeof text !== "string") return "";
|
|
36
|
+
// Remove wrapping quotes and extra whitespace
|
|
37
|
+
return text.replace(/^["']|["']$/g, "").trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Semantic Doc Search Resolver
|
|
42
|
+
*/
|
|
30
43
|
async function resolver(action, context = {}) {
|
|
31
44
|
if (typeof action !== "string") return;
|
|
32
45
|
if (!action.toLowerCase().startsWith("ask doc-search")) return;
|
|
33
46
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!query) throw new Error("Query is empty after sanitization");
|
|
47
|
+
let query = extractQuery(action);
|
|
48
|
+
query = sanitizeTextForEmbedding(query);
|
|
49
|
+
if (!query) return { text: "(Empty query)", meta: { matches: 0 } };
|
|
38
50
|
|
|
39
51
|
// Vector backend
|
|
40
52
|
const vectorStore = VectorRouter.create(context);
|
|
@@ -61,14 +73,14 @@ async function resolver(action, context = {}) {
|
|
|
61
73
|
for (const doc of context.documents) {
|
|
62
74
|
const chunks = doc.chunks || [doc.content];
|
|
63
75
|
for (let i = 0; i < chunks.length; i++) {
|
|
64
|
-
const text = chunks[i];
|
|
65
|
-
if (!text
|
|
76
|
+
const text = sanitizeTextForEmbedding(chunks[i]);
|
|
77
|
+
if (!text) continue;
|
|
66
78
|
|
|
67
79
|
const hash = hashContent(text);
|
|
68
80
|
if (cache[hash]) continue; // Skip already ingested
|
|
69
81
|
|
|
70
82
|
const vector = await embedder.embed(text);
|
|
71
|
-
if (!
|
|
83
|
+
if (!vector || vector.every(v => v === 0)) continue;
|
|
72
84
|
|
|
73
85
|
await vectorStore.upsert({
|
|
74
86
|
id: `${doc.id}:${i}`,
|
|
@@ -85,10 +97,12 @@ async function resolver(action, context = {}) {
|
|
|
85
97
|
|
|
86
98
|
// Embed query & search
|
|
87
99
|
const queryVector = await embedder.embed(query);
|
|
88
|
-
if (!
|
|
89
|
-
|
|
100
|
+
if (!queryVector || queryVector.every(v => v === 0)) {
|
|
101
|
+
console.warn("⚠️ Query embedding invalid");
|
|
102
|
+
return { text: "(Query could not be embedded)", meta: { matches: 0 } };
|
|
90
103
|
}
|
|
91
104
|
|
|
105
|
+
// Top-K + similarity threshold
|
|
92
106
|
const results = await vectorStore.query({
|
|
93
107
|
vector: queryVector,
|
|
94
108
|
topK: context.topK || 5,
|
package/test-embed.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// test-embed.js
|
|
2
|
+
const embedder = require("./src/embeddings/local");
|
|
3
|
+
|
|
4
|
+
async function test() {
|
|
5
|
+
console.log("Model dimension:", embedder.getDimension());
|
|
6
|
+
const vector = await embedder.embed("hello world");
|
|
7
|
+
console.log("Embedding result:", vector?.length, vector);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test().catch(console.error);
|