@modusensus/dsh-mneme 0.1.6 → 0.2.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/LICENSE +21 -21
- package/README.md +239 -207
- package/cordis.patch.yml +15 -15
- package/lib/api.js +277 -245
- package/lib/client.js +461 -461
- package/lib/commands.js +64 -64
- package/lib/config.js +49 -15
- package/lib/dream/clustering.js +118 -0
- package/lib/dream/decisions.js +128 -121
- package/lib/dream.js +428 -209
- package/lib/embedding.js +97 -97
- package/lib/index.js +176 -120
- package/lib/inject.js +47 -47
- package/lib/local-embedder.js +265 -0
- package/lib/mirror.js +131 -131
- package/lib/reranker.js +203 -0
- package/lib/service.js +268 -174
- package/lib/settings.js +142 -142
- package/lib/store.js +409 -310
- package/lib/summarize.js +171 -171
- package/lib/tools.js +230 -241
- package/lib/vector-index.js +106 -0
- package/package.json +7 -3
- package/src/api.js +277 -245
- package/src/commands.js +64 -64
- package/src/config.js +49 -15
- package/src/dream/clustering.js +118 -0
- package/src/dream/decisions.js +128 -121
- package/src/dream.js +428 -209
- package/src/embedding.js +97 -97
- package/src/index.js +176 -120
- package/src/inject.js +47 -47
- package/src/local-embedder.js +265 -0
- package/src/mirror.js +131 -131
- package/src/reranker.js +203 -0
- package/src/service.js +268 -174
- package/src/settings.js +142 -142
- package/src/store.js +409 -310
- package/src/summarize.js +171 -171
- package/src/tools.js +230 -241
- package/src/vector-index.js +106 -0
package/lib/api.js
CHANGED
|
@@ -1,245 +1,277 @@
|
|
|
1
|
-
import { URL } from "node:url";
|
|
2
|
-
|
|
3
|
-
function sendJson(res, status, payload) {
|
|
4
|
-
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
5
|
-
res.end(JSON.stringify(payload));
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Collect the request body as text (tolerant of empty/invalid bodies). */
|
|
9
|
-
function readBody(req) {
|
|
10
|
-
return new Promise((resolve) => {
|
|
11
|
-
let body = "";
|
|
12
|
-
req.on("data", (chunk) => { body += chunk; });
|
|
13
|
-
req.on("end", () => resolve(body));
|
|
14
|
-
req.on("error", () => resolve(""));
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function parseBody(text) {
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(text || "{}");
|
|
21
|
-
} catch {
|
|
22
|
-
return {};
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createApi(ctx, service, settings, commands, embedder) {
|
|
27
|
-
const disposers = [];
|
|
28
|
-
|
|
29
|
-
const register = (route) => {
|
|
30
|
-
disposers.push(ctx.webServer.register(route));
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// /api/dsh-mneme prefix fallback → 404 JSON for unknown sub-paths
|
|
34
|
-
register({
|
|
35
|
-
kind: "prefix",
|
|
36
|
-
path: "/api/dsh-mneme",
|
|
37
|
-
handler(req, res) {
|
|
38
|
-
sendJson(res, 404, { error: "not-found" });
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
register({
|
|
43
|
-
kind: "exact",
|
|
44
|
-
path: "/api/dsh-mneme/list",
|
|
45
|
-
handler(req, res) {
|
|
46
|
-
try {
|
|
47
|
-
const url = new URL(req.url, "http://localhost");
|
|
48
|
-
const type = url.searchParams.get("type") ?? undefined;
|
|
49
|
-
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
50
|
-
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
51
|
-
const items = service.toApiList(service.list({ type, limit, offset }));
|
|
52
|
-
sendJson(res, 200, { items, total: service.count(type) });
|
|
53
|
-
} catch {
|
|
54
|
-
sendJson(res, 500, { error: "internal" });
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
register({
|
|
60
|
-
kind: "exact",
|
|
61
|
-
path: "/api/dsh-mneme/search",
|
|
62
|
-
handler(req, res) {
|
|
63
|
-
try {
|
|
64
|
-
const url = new URL(req.url, "http://localhost");
|
|
65
|
-
const q = url.searchParams.get("q") ?? "";
|
|
66
|
-
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
67
|
-
// mode: auto (default) | keyword | vector
|
|
68
|
-
const mode = url.searchParams.get("mode") ?? "auto";
|
|
69
|
-
const query = q.trim();
|
|
70
|
-
if (!query) {
|
|
71
|
-
sendJson(res, 200, { items: [], mode: "keyword" });
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
// Keyword results (existing behavior) always computed; used as a
|
|
75
|
-
// fallback and as the primary ranking when vector is unavailable.
|
|
76
|
-
const keyword = service.toApiList(service.search(query, { limit }));
|
|
77
|
-
if (mode === "keyword" || !embedder) {
|
|
78
|
-
sendJson(res, 200, { items: keyword, mode: "keyword" });
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
const cfg = settings.getVectorConfig();
|
|
82
|
-
if (mode === "vector" && !cfg?.enabled) {
|
|
83
|
-
sendJson(res, 200, { items: keyword, mode: "keyword", error: "vector-disabled" });
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
// Try vector search; on any failure fall back to keyword results.
|
|
87
|
-
return embedder.embed(query).then(async (vector) => {
|
|
88
|
-
let items = keyword;
|
|
89
|
-
let used = "keyword";
|
|
90
|
-
if (vector) {
|
|
91
|
-
const scored = service.toApiList(service.searchVector(vector, { limit }));
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
1
|
+
import { URL } from "node:url";
|
|
2
|
+
|
|
3
|
+
function sendJson(res, status, payload) {
|
|
4
|
+
res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
|
|
5
|
+
res.end(JSON.stringify(payload));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Collect the request body as text (tolerant of empty/invalid bodies). */
|
|
9
|
+
function readBody(req) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
let body = "";
|
|
12
|
+
req.on("data", (chunk) => { body += chunk; });
|
|
13
|
+
req.on("end", () => resolve(body));
|
|
14
|
+
req.on("error", () => resolve(""));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseBody(text) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(text || "{}");
|
|
21
|
+
} catch {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createApi(ctx, service, settings, commands, embedder, semantic = null) {
|
|
27
|
+
const disposers = [];
|
|
28
|
+
|
|
29
|
+
const register = (route) => {
|
|
30
|
+
disposers.push(ctx.webServer.register(route));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// /api/dsh-mneme prefix fallback → 404 JSON for unknown sub-paths
|
|
34
|
+
register({
|
|
35
|
+
kind: "prefix",
|
|
36
|
+
path: "/api/dsh-mneme",
|
|
37
|
+
handler(req, res) {
|
|
38
|
+
sendJson(res, 404, { error: "not-found" });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
register({
|
|
43
|
+
kind: "exact",
|
|
44
|
+
path: "/api/dsh-mneme/list",
|
|
45
|
+
handler(req, res) {
|
|
46
|
+
try {
|
|
47
|
+
const url = new URL(req.url, "http://localhost");
|
|
48
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
49
|
+
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
50
|
+
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
51
|
+
const items = service.toApiList(service.list({ type, limit, offset }));
|
|
52
|
+
sendJson(res, 200, { items, total: service.count(type) });
|
|
53
|
+
} catch {
|
|
54
|
+
sendJson(res, 500, { error: "internal" });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
register({
|
|
60
|
+
kind: "exact",
|
|
61
|
+
path: "/api/dsh-mneme/search",
|
|
62
|
+
handler(req, res) {
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(req.url, "http://localhost");
|
|
65
|
+
const q = url.searchParams.get("q") ?? "";
|
|
66
|
+
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
67
|
+
// mode: auto (default) | keyword | vector | hybrid
|
|
68
|
+
const mode = url.searchParams.get("mode") ?? "auto";
|
|
69
|
+
const query = q.trim();
|
|
70
|
+
if (!query) {
|
|
71
|
+
sendJson(res, 200, { items: [], mode: "keyword" });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Keyword results (existing behavior) always computed; used as a
|
|
75
|
+
// fallback and as the primary ranking when vector is unavailable.
|
|
76
|
+
const keyword = service.toApiList(service.search(query, { limit }));
|
|
77
|
+
if (mode === "keyword" || !embedder) {
|
|
78
|
+
sendJson(res, 200, { items: keyword, mode: "keyword" });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const cfg = settings.getVectorConfig();
|
|
82
|
+
if (mode === "vector" && !cfg?.enabled) {
|
|
83
|
+
sendJson(res, 200, { items: keyword, mode: "keyword", error: "vector-disabled" });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// Try vector search; on any failure fall back to keyword results.
|
|
87
|
+
return embedder.embed(query).then(async (vector) => {
|
|
88
|
+
let items = keyword;
|
|
89
|
+
let used = "keyword";
|
|
90
|
+
if (vector) {
|
|
91
|
+
const scored = service.toApiList(service.searchVector(vector, { limit }));
|
|
92
|
+
if (mode === "hybrid") {
|
|
93
|
+
// hybrid: vector recalls lead, keyword fills remaining slots
|
|
94
|
+
const seen = new Set(scored.map((m) => m.id));
|
|
95
|
+
const merged = [...scored.slice(0, limit)];
|
|
96
|
+
for (const m of keyword) {
|
|
97
|
+
if (merged.length >= limit) break;
|
|
98
|
+
if (!seen.has(m.id)) { seen.add(m.id); merged.push(m); }
|
|
99
|
+
}
|
|
100
|
+
items = merged;
|
|
101
|
+
used = "vector";
|
|
102
|
+
} else {
|
|
103
|
+
// auto/vector: keyword exact hits first (the user's literal
|
|
104
|
+
// words), then vector results fill the remaining slots, deduped.
|
|
105
|
+
const seen = new Set(keyword.map((m) => m.id));
|
|
106
|
+
const merged = [...keyword];
|
|
107
|
+
for (const m of scored) {
|
|
108
|
+
if (merged.length >= limit) break;
|
|
109
|
+
if (!seen.has(m.id)) {
|
|
110
|
+
seen.add(m.id);
|
|
111
|
+
merged.push(m);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
items = merged;
|
|
115
|
+
used = "vector";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
sendJson(res, 200, { items, mode: used });
|
|
119
|
+
}).catch(() => {
|
|
120
|
+
sendJson(res, 200, { items: keyword, mode: "keyword" });
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
sendJson(res, 500, { error: "internal" });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// --- user profile ---
|
|
129
|
+
register({
|
|
130
|
+
kind: "exact",
|
|
131
|
+
path: "/api/dsh-mneme/profile",
|
|
132
|
+
handler(req, res) {
|
|
133
|
+
try {
|
|
134
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
135
|
+
return readBody(req).then((text) => {
|
|
136
|
+
const body = parseBody(text);
|
|
137
|
+
settings.setProfile(typeof body.profile === "string" ? body.profile : "");
|
|
138
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
142
|
+
} catch {
|
|
143
|
+
sendJson(res, 500, { error: "internal" });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// --- rules ---
|
|
149
|
+
register({
|
|
150
|
+
kind: "exact",
|
|
151
|
+
path: "/api/dsh-mneme/rules",
|
|
152
|
+
handler(req, res) {
|
|
153
|
+
try {
|
|
154
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
155
|
+
return readBody(req).then((text) => {
|
|
156
|
+
const body = parseBody(text);
|
|
157
|
+
settings.setRules(Array.isArray(body.rules) ? body.rules : []);
|
|
158
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
162
|
+
} catch {
|
|
163
|
+
sendJson(res, 500, { error: "internal" });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// --- vector search config ---
|
|
169
|
+
register({
|
|
170
|
+
kind: "exact",
|
|
171
|
+
path: "/api/dsh-mneme/vector-config",
|
|
172
|
+
handler(req, res) {
|
|
173
|
+
try {
|
|
174
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
175
|
+
return readBody(req).then((text) => {
|
|
176
|
+
const body = parseBody(text);
|
|
177
|
+
const cfg = settings.setVectorConfig({
|
|
178
|
+
enabled: body.enabled,
|
|
179
|
+
baseUrl: body.baseUrl,
|
|
180
|
+
apiKey: body.apiKey,
|
|
181
|
+
model: body.model
|
|
182
|
+
});
|
|
183
|
+
sendJson(res, 200, { config: cfg });
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
sendJson(res, 200, { config: settings.getVectorConfig() ?? { enabled: false, baseUrl: "", apiKey: "", model: "" } });
|
|
187
|
+
} catch {
|
|
188
|
+
sendJson(res, 500, { error: "internal" });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- vector re-index (backfill embeddings for rows missing them) ---
|
|
194
|
+
register({
|
|
195
|
+
kind: "exact",
|
|
196
|
+
path: "/api/dsh-mneme/vector-reindex",
|
|
197
|
+
handler(req, res) {
|
|
198
|
+
try {
|
|
199
|
+
if (!embedder) {
|
|
200
|
+
sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-unavailable" });
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const url = new URL(req.url, "http://localhost");
|
|
204
|
+
const limit = Number(url.searchParams.get("limit") ?? 100);
|
|
205
|
+
embedder.reindexMissing(limit).then((result) => {
|
|
206
|
+
sendJson(res, 200, result);
|
|
207
|
+
}).catch(() => {
|
|
208
|
+
sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-failed" });
|
|
209
|
+
});
|
|
210
|
+
} catch {
|
|
211
|
+
sendJson(res, 500, { error: "internal" });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// --- semantic pipeline status (model, index, reranker) ---
|
|
217
|
+
register({
|
|
218
|
+
kind: "exact",
|
|
219
|
+
path: "/api/dsh-mneme/semantic",
|
|
220
|
+
handler(req, res) {
|
|
221
|
+
try {
|
|
222
|
+
const stats = semantic?.vectorIndex?.getStats?.() ?? null;
|
|
223
|
+
sendJson(res, 200, {
|
|
224
|
+
embedProvider: embedder ? (embedder.constructor?.name ?? "unknown") : null,
|
|
225
|
+
modelHash: embedder?.modelHash ?? null,
|
|
226
|
+
dimension: embedder?.dimension ?? null,
|
|
227
|
+
reranker: semantic?.reranker ? "ready" : null,
|
|
228
|
+
index: stats
|
|
229
|
+
});
|
|
230
|
+
} catch {
|
|
231
|
+
sendJson(res, 500, { error: "internal" });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// --- custom commands ---
|
|
237
|
+
register({
|
|
238
|
+
kind: "exact",
|
|
239
|
+
path: "/api/dsh-mneme/commands",
|
|
240
|
+
handler(req, res) {
|
|
241
|
+
try {
|
|
242
|
+
if (req.method === "POST") {
|
|
243
|
+
return readBody(req).then((text) => {
|
|
244
|
+
const body = parseBody(text);
|
|
245
|
+
try {
|
|
246
|
+
const command = commands.add({
|
|
247
|
+
name: body.name,
|
|
248
|
+
description: body.description,
|
|
249
|
+
instruction: body.instruction
|
|
250
|
+
});
|
|
251
|
+
sendJson(res, 200, { command });
|
|
252
|
+
} catch (error) {
|
|
253
|
+
sendJson(res, 400, { error: error.message });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (req.method === "DELETE") {
|
|
258
|
+
const url = new URL(req.url, "http://localhost");
|
|
259
|
+
const id = url.searchParams.get("id");
|
|
260
|
+
const removed = id ? commands.remove(id) : false;
|
|
261
|
+
sendJson(res, 200, { removed });
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
sendJson(res, 200, { commands: commands.list() });
|
|
265
|
+
} catch {
|
|
266
|
+
sendJson(res, 500, { error: "internal" });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
routes: 7,
|
|
273
|
+
dispose: () => {
|
|
274
|
+
for (const dispose of disposers) dispose();
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|