bc-observatory 0.2.0 → 0.3.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 +18 -3
- package/dist/server.js +1231 -265
- package/package.json +6 -3
- package/dist/embed.js +0 -133
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bc-observatory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for BC Observatory: an agent-first, cross-referenced knowledge base of Microsoft Dynamics 365 Business Central (Learn, code, roadmap, videos, community blogs).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,12 +28,15 @@
|
|
|
28
28
|
"model-context-protocol"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsc -p tsconfig.json &&
|
|
31
|
+
"build": "tsc -p tsconfig.json --noEmit && tsx build.ts",
|
|
32
32
|
"start": "node dist/server.js"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
36
|
-
"minisearch": "^7.2.0",
|
|
37
36
|
"zod": "^4.6.5"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@bc-observatory/search": "0.1.0",
|
|
40
|
+
"esbuild": "^0.28.2"
|
|
38
41
|
}
|
|
39
42
|
}
|
package/dist/embed.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
export const MAX_TOKENS = 512;
|
|
2
|
-
/** tokenizer.json (WordPiece), model.safetensors (one F32 tensor) and config.json into a model. */
|
|
3
|
-
export function parseModel(tokenizerJson, safetensors, config = {}) {
|
|
4
|
-
const t = JSON.parse(tokenizerJson);
|
|
5
|
-
if (t.model.type !== "WordPiece")
|
|
6
|
-
throw new Error(`unsupported tokenizer model ${t.model.type}`);
|
|
7
|
-
const vocab = new Map(Object.entries(t.model.vocab));
|
|
8
|
-
const view = new DataView(safetensors.buffer, safetensors.byteOffset, safetensors.byteLength);
|
|
9
|
-
const headerLen = Number(view.getBigUint64(0, true));
|
|
10
|
-
const header = JSON.parse(new TextDecoder().decode(safetensors.subarray(8, 8 + headerLen)));
|
|
11
|
-
const name = Object.keys(header).find((k) => k !== "__metadata__");
|
|
12
|
-
if (!name)
|
|
13
|
-
throw new Error("safetensors file has no tensor");
|
|
14
|
-
const { dtype, shape, data_offsets: [start, end] } = header[name];
|
|
15
|
-
if (dtype !== "F32" || shape.length !== 2)
|
|
16
|
-
throw new Error(`unsupported tensor ${name}: ${dtype} ${shape.join("x")}`);
|
|
17
|
-
// copy into an aligned buffer: the data starts at 8 + headerLen, which need not be a multiple of 4
|
|
18
|
-
const bytes = safetensors.slice(8 + headerLen + start, 8 + headerLen + end);
|
|
19
|
-
const matrix = new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4);
|
|
20
|
-
if (matrix.length !== shape[0] * shape[1])
|
|
21
|
-
throw new Error("tensor size does not match its shape");
|
|
22
|
-
const unk = t.model.unk_token !== undefined ? (vocab.get(t.model.unk_token) ?? null) : null;
|
|
23
|
-
return { dims: shape[1], vocab, unk, matrix, normalize: config.normalize !== false, maxWordChars: t.model.max_input_chars_per_word ?? 100, prefix: t.model.continuing_subword_prefix ?? "##" };
|
|
24
|
-
}
|
|
25
|
-
const CJK = [[0x4e00, 0x9fff], [0x3400, 0x4dbf], [0x20000, 0x2a6df], [0x2a700, 0x2b73f], [0x2b740, 0x2b81f], [0x2b820, 0x2ceaf], [0xf900, 0xfaff], [0x2f800, 0x2fa1f]];
|
|
26
|
-
const isCjk = (cp) => CJK.some(([a, b]) => cp >= a && cp <= b);
|
|
27
|
-
const isPunct = (ch, cp) => (cp >= 33 && cp <= 47) || (cp >= 58 && cp <= 64) || (cp >= 91 && cp <= 96) || (cp >= 123 && cp <= 126) || /\p{P}/u.test(ch);
|
|
28
|
-
/** BertNormalizer: clean text, CJK ideographs spaced out, accents stripped, lowercased. */
|
|
29
|
-
export function normalize(text) {
|
|
30
|
-
let out = "";
|
|
31
|
-
for (const ch of text) {
|
|
32
|
-
const cp = ch.codePointAt(0);
|
|
33
|
-
if (cp === 0 || cp === 0xfffd)
|
|
34
|
-
continue;
|
|
35
|
-
if (ch === "\t" || ch === "\n" || ch === "\r" || /\s/u.test(ch)) {
|
|
36
|
-
out += " ";
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
if (/[\p{Cc}\p{Cf}]/u.test(ch))
|
|
40
|
-
continue;
|
|
41
|
-
out += isCjk(cp) ? ` ${ch} ` : ch;
|
|
42
|
-
}
|
|
43
|
-
return out.normalize("NFD").replace(/\p{Mn}/gu, "").toLowerCase();
|
|
44
|
-
}
|
|
45
|
-
/** BertPreTokenizer: words split on whitespace, every punctuation character a word of its own. */
|
|
46
|
-
export function preTokenize(text) {
|
|
47
|
-
const words = [];
|
|
48
|
-
let cur = "";
|
|
49
|
-
for (const ch of text) {
|
|
50
|
-
if (/\s/u.test(ch)) {
|
|
51
|
-
if (cur)
|
|
52
|
-
words.push(cur);
|
|
53
|
-
cur = "";
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
if (isPunct(ch, ch.codePointAt(0))) {
|
|
57
|
-
if (cur)
|
|
58
|
-
words.push(cur);
|
|
59
|
-
words.push(ch);
|
|
60
|
-
cur = "";
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
cur += ch;
|
|
64
|
-
}
|
|
65
|
-
if (cur)
|
|
66
|
-
words.push(cur);
|
|
67
|
-
return words;
|
|
68
|
-
}
|
|
69
|
-
/** Token ids of a text: no special tokens, no [UNK], at most MAX_TOKENS. */
|
|
70
|
-
export function tokenize(m, text) {
|
|
71
|
-
const ids = [];
|
|
72
|
-
for (const word of preTokenize(normalize(text))) {
|
|
73
|
-
const chars = [...word];
|
|
74
|
-
if (chars.length > m.maxWordChars)
|
|
75
|
-
continue; // the whole word would be [UNK], which is dropped
|
|
76
|
-
const pieces = [];
|
|
77
|
-
let start = 0, bad = false;
|
|
78
|
-
while (start < chars.length) {
|
|
79
|
-
let end = chars.length, found;
|
|
80
|
-
while (start < end) {
|
|
81
|
-
const sub = (start > 0 ? m.prefix : "") + chars.slice(start, end).join("");
|
|
82
|
-
found = m.vocab.get(sub);
|
|
83
|
-
if (found !== undefined)
|
|
84
|
-
break;
|
|
85
|
-
end--;
|
|
86
|
-
}
|
|
87
|
-
if (found === undefined) {
|
|
88
|
-
bad = true;
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
pieces.push(found);
|
|
92
|
-
start = end;
|
|
93
|
-
}
|
|
94
|
-
if (!bad)
|
|
95
|
-
for (const id of pieces)
|
|
96
|
-
if (id !== m.unk)
|
|
97
|
-
ids.push(id);
|
|
98
|
-
if (ids.length >= MAX_TOKENS)
|
|
99
|
-
return ids.slice(0, MAX_TOKENS);
|
|
100
|
-
}
|
|
101
|
-
return ids;
|
|
102
|
-
}
|
|
103
|
-
/** The text's vector: mean of its tokens' rows, L2-normalized (all zeros for a text with no known token). */
|
|
104
|
-
export function embed(m, text) {
|
|
105
|
-
const v = new Float32Array(m.dims);
|
|
106
|
-
const ids = tokenize(m, text);
|
|
107
|
-
if (!ids.length)
|
|
108
|
-
return v;
|
|
109
|
-
for (const id of ids) {
|
|
110
|
-
const off = id * m.dims;
|
|
111
|
-
for (let d = 0; d < m.dims; d++)
|
|
112
|
-
v[d] += m.matrix[off + d];
|
|
113
|
-
}
|
|
114
|
-
for (let d = 0; d < m.dims; d++)
|
|
115
|
-
v[d] /= ids.length;
|
|
116
|
-
if (m.normalize) {
|
|
117
|
-
let n = 0;
|
|
118
|
-
for (let d = 0; d < m.dims; d++)
|
|
119
|
-
n += v[d] * v[d];
|
|
120
|
-
n = Math.sqrt(n);
|
|
121
|
-
if (n > 0)
|
|
122
|
-
for (let d = 0; d < m.dims; d++)
|
|
123
|
-
v[d] /= n;
|
|
124
|
-
}
|
|
125
|
-
return v;
|
|
126
|
-
}
|
|
127
|
-
/** Dot product: the cosine similarity of two normalized vectors. */
|
|
128
|
-
export function dot(a, b, bOffset = 0) {
|
|
129
|
-
let s = 0;
|
|
130
|
-
for (let d = 0; d < a.length; d++)
|
|
131
|
-
s += a[d] * b[bOffset + d];
|
|
132
|
-
return s;
|
|
133
|
-
}
|