@modusensus/dsh-mneme 0.1.6 → 0.2.1
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 +256 -245
- package/lib/client.js +461 -461
- package/lib/commands.js +64 -64
- package/lib/config.js +55 -15
- package/lib/dream/clustering.js +118 -0
- package/lib/dream/decisions.js +177 -121
- package/lib/dream.js +467 -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 +210 -0
- package/lib/service.js +324 -174
- package/lib/settings.js +142 -142
- package/lib/store.js +466 -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 +256 -245
- package/src/commands.js +64 -64
- package/src/config.js +55 -15
- package/src/dream/clustering.js +118 -0
- package/src/dream/decisions.js +177 -121
- package/src/dream.js +467 -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 +210 -0
- package/src/service.js +324 -174
- package/src/settings.js +142 -142
- package/src/store.js +466 -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,256 @@
|
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
// Ensure the service has an embedder when the API layer was handed one
|
|
30
|
+
// (tests wire the embedder through the API instead of index.js). Without
|
|
31
|
+
// this, /api/dsh-mneme/search would silently degrade to keyword-only.
|
|
32
|
+
if (embedder && typeof service.setEmbedder === "function") {
|
|
33
|
+
service.setEmbedder(embedder);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const register = (route) => {
|
|
37
|
+
disposers.push(ctx.webServer.register(route));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// /api/dsh-mneme prefix fallback → 404 JSON for unknown sub-paths
|
|
41
|
+
register({
|
|
42
|
+
kind: "prefix",
|
|
43
|
+
path: "/api/dsh-mneme",
|
|
44
|
+
handler(req, res) {
|
|
45
|
+
sendJson(res, 404, { error: "not-found" });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
register({
|
|
50
|
+
kind: "exact",
|
|
51
|
+
path: "/api/dsh-mneme/list",
|
|
52
|
+
handler(req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(req.url, "http://localhost");
|
|
55
|
+
const type = url.searchParams.get("type") ?? undefined;
|
|
56
|
+
const limit = Number(url.searchParams.get("limit") ?? 50);
|
|
57
|
+
const offset = Number(url.searchParams.get("offset") ?? 0);
|
|
58
|
+
const items = service.toApiList(service.list({ type, limit, offset }));
|
|
59
|
+
sendJson(res, 200, { items, total: service.count(type) });
|
|
60
|
+
} catch {
|
|
61
|
+
sendJson(res, 500, { error: "internal" });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
register({
|
|
67
|
+
kind: "exact",
|
|
68
|
+
path: "/api/dsh-mneme/search",
|
|
69
|
+
handler(req, res) {
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(req.url, "http://localhost");
|
|
72
|
+
const q = url.searchParams.get("q") ?? "";
|
|
73
|
+
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
74
|
+
// mode: auto (default) | keyword | vector | hybrid
|
|
75
|
+
const mode = url.searchParams.get("mode") ?? "auto";
|
|
76
|
+
const rerank = url.searchParams.get("rerank") !== "false";
|
|
77
|
+
const query = q.trim();
|
|
78
|
+
if (!query) {
|
|
79
|
+
sendJson(res, 200, { items: [], mode: "keyword" });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
// Route through the unified semantic pipeline; any vector/rerank
|
|
83
|
+
// failure degrades to keyword results inside searchMemories. The
|
|
84
|
+
// returned promise lets the test double await the async search.
|
|
85
|
+
return Promise.resolve(
|
|
86
|
+
service.searchMemories(query, { mode, topK: limit, useRerank: rerank })
|
|
87
|
+
).then((rows) => {
|
|
88
|
+
// mode reflects what actually happened: rows marked `vector` came
|
|
89
|
+
// through the semantic path, everything else is keyword fallback.
|
|
90
|
+
const used = rows.some((m) => m.vector === true) ? "vector" : "keyword";
|
|
91
|
+
sendJson(res, 200, { items: service.toApiList(rows), mode: used });
|
|
92
|
+
}).catch(() => {
|
|
93
|
+
sendJson(res, 200, { items: service.toApiList(service.search(query, { limit })), mode: "keyword" });
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
sendJson(res, 500, { error: "internal" });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// --- user profile ---
|
|
102
|
+
register({
|
|
103
|
+
kind: "exact",
|
|
104
|
+
path: "/api/dsh-mneme/profile",
|
|
105
|
+
handler(req, res) {
|
|
106
|
+
try {
|
|
107
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
108
|
+
return readBody(req).then((text) => {
|
|
109
|
+
const body = parseBody(text);
|
|
110
|
+
settings.setProfile(typeof body.profile === "string" ? body.profile : "");
|
|
111
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
sendJson(res, 200, { profile: settings.getProfile() });
|
|
115
|
+
} catch {
|
|
116
|
+
sendJson(res, 500, { error: "internal" });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// --- rules ---
|
|
122
|
+
register({
|
|
123
|
+
kind: "exact",
|
|
124
|
+
path: "/api/dsh-mneme/rules",
|
|
125
|
+
handler(req, res) {
|
|
126
|
+
try {
|
|
127
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
128
|
+
return readBody(req).then((text) => {
|
|
129
|
+
const body = parseBody(text);
|
|
130
|
+
settings.setRules(Array.isArray(body.rules) ? body.rules : []);
|
|
131
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
sendJson(res, 200, { rules: settings.getRules() });
|
|
135
|
+
} catch {
|
|
136
|
+
sendJson(res, 500, { error: "internal" });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// --- vector search config ---
|
|
142
|
+
register({
|
|
143
|
+
kind: "exact",
|
|
144
|
+
path: "/api/dsh-mneme/vector-config",
|
|
145
|
+
handler(req, res) {
|
|
146
|
+
try {
|
|
147
|
+
if (req.method === "PUT" || req.method === "POST") {
|
|
148
|
+
return readBody(req).then((text) => {
|
|
149
|
+
const body = parseBody(text);
|
|
150
|
+
const cfg = settings.setVectorConfig({
|
|
151
|
+
enabled: body.enabled,
|
|
152
|
+
baseUrl: body.baseUrl,
|
|
153
|
+
apiKey: body.apiKey,
|
|
154
|
+
model: body.model
|
|
155
|
+
});
|
|
156
|
+
sendJson(res, 200, { config: cfg });
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
sendJson(res, 200, { config: settings.getVectorConfig() ?? { enabled: false, baseUrl: "", apiKey: "", model: "" } });
|
|
160
|
+
} catch {
|
|
161
|
+
sendJson(res, 500, { error: "internal" });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// --- vector re-index (backfill embeddings for rows missing them) ---
|
|
167
|
+
register({
|
|
168
|
+
kind: "exact",
|
|
169
|
+
path: "/api/dsh-mneme/vector-reindex",
|
|
170
|
+
handler(req, res) {
|
|
171
|
+
try {
|
|
172
|
+
if (!embedder) {
|
|
173
|
+
sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-unavailable" });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const url = new URL(req.url, "http://localhost");
|
|
177
|
+
const limit = Number(url.searchParams.get("limit") ?? 100);
|
|
178
|
+
// Unified re-index entry: works for both the legacy OpenAI embedder and
|
|
179
|
+
// the new local/ollama backends (which have no reindexMissing method).
|
|
180
|
+
const viaIndex = semantic?.vectorIndex && semantic?.vectorIndex.rebuildIndex;
|
|
181
|
+
const task = viaIndex
|
|
182
|
+
? semantic.vectorIndex.rebuildIndex(embedder, { limit })
|
|
183
|
+
: embedder.reindexMissing ? embedder.reindexMissing(limit) : Promise.resolve({ indexed: 0, skipped: 0, error: "vector-unavailable" });
|
|
184
|
+
task.then((result) => {
|
|
185
|
+
sendJson(res, 200, result);
|
|
186
|
+
}).catch(() => {
|
|
187
|
+
sendJson(res, 200, { indexed: 0, skipped: 0, error: "vector-failed" });
|
|
188
|
+
});
|
|
189
|
+
} catch {
|
|
190
|
+
sendJson(res, 500, { error: "internal" });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// --- semantic pipeline status (model, index, reranker) ---
|
|
196
|
+
register({
|
|
197
|
+
kind: "exact",
|
|
198
|
+
path: "/api/dsh-mneme/semantic",
|
|
199
|
+
handler(req, res) {
|
|
200
|
+
try {
|
|
201
|
+
const stats = semantic?.vectorIndex?.getStats?.() ?? null;
|
|
202
|
+
sendJson(res, 200, {
|
|
203
|
+
embedProvider: embedder ? (embedder.constructor?.name ?? "unknown") : null,
|
|
204
|
+
modelHash: embedder?.modelHash ?? null,
|
|
205
|
+
dimension: embedder?.dimension ?? null,
|
|
206
|
+
reranker: semantic?.reranker ? "ready" : null,
|
|
207
|
+
index: stats
|
|
208
|
+
});
|
|
209
|
+
} catch {
|
|
210
|
+
sendJson(res, 500, { error: "internal" });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// --- custom commands ---
|
|
216
|
+
register({
|
|
217
|
+
kind: "exact",
|
|
218
|
+
path: "/api/dsh-mneme/commands",
|
|
219
|
+
handler(req, res) {
|
|
220
|
+
try {
|
|
221
|
+
if (req.method === "POST") {
|
|
222
|
+
return readBody(req).then((text) => {
|
|
223
|
+
const body = parseBody(text);
|
|
224
|
+
try {
|
|
225
|
+
const command = commands.add({
|
|
226
|
+
name: body.name,
|
|
227
|
+
description: body.description,
|
|
228
|
+
instruction: body.instruction
|
|
229
|
+
});
|
|
230
|
+
sendJson(res, 200, { command });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
sendJson(res, 400, { error: error.message });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (req.method === "DELETE") {
|
|
237
|
+
const url = new URL(req.url, "http://localhost");
|
|
238
|
+
const id = url.searchParams.get("id");
|
|
239
|
+
const removed = id ? commands.remove(id) : false;
|
|
240
|
+
sendJson(res, 200, { removed });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
sendJson(res, 200, { commands: commands.list() });
|
|
244
|
+
} catch {
|
|
245
|
+
sendJson(res, 500, { error: "internal" });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
routes: 7,
|
|
252
|
+
dispose: () => {
|
|
253
|
+
for (const dispose of disposers) dispose();
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|