@diegoaltoworks/chatter 0.60.0 → 0.62.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 +56 -17
- package/dist/auth/secretStrength.d.ts +1 -1
- package/dist/auth/secretStrength.d.ts.map +1 -1
- package/dist/bin/create-apikey.js +45 -28
- package/dist/bin/wa-pair.js +113 -46
- package/dist/channels/backoff.d.ts +10 -0
- package/dist/channels/backoff.d.ts.map +1 -0
- package/dist/channels/gates.d.ts +15 -8
- package/dist/channels/gates.d.ts.map +1 -1
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/index.d.ts.map +1 -1
- package/dist/channels/index.js +38 -5
- package/dist/channels/index.mjs +37 -5
- package/dist/channels/matrix/api.d.ts +152 -0
- package/dist/channels/matrix/api.d.ts.map +1 -0
- package/dist/channels/matrix/channel.d.ts +46 -0
- package/dist/channels/matrix/channel.d.ts.map +1 -0
- package/dist/channels/matrix/handler.d.ts +58 -0
- package/dist/channels/matrix/handler.d.ts.map +1 -0
- package/dist/channels/matrix/index.d.ts +37 -0
- package/dist/channels/matrix/index.d.ts.map +1 -0
- package/dist/channels/matrix/index.js +1128 -0
- package/dist/channels/matrix/index.mjs +1079 -0
- package/dist/channels/matrix/sync.d.ts +53 -0
- package/dist/channels/matrix/sync.d.ts.map +1 -0
- package/dist/channels/matrix/updates.d.ts +104 -0
- package/dist/channels/matrix/updates.d.ts.map +1 -0
- package/dist/channels/pipeline.d.ts +14 -14
- package/dist/channels/pipeline.d.ts.map +1 -1
- package/dist/channels/polling.d.ts +67 -0
- package/dist/channels/polling.d.ts.map +1 -0
- package/dist/channels/telegram/channel.d.ts +2 -56
- package/dist/channels/telegram/channel.d.ts.map +1 -1
- package/dist/channels/telegram/index.js +53 -23
- package/dist/channels/telegram/index.mjs +53 -23
- package/dist/channels/telegram/poll.d.ts +5 -1
- package/dist/channels/telegram/poll.d.ts.map +1 -1
- package/dist/channels/telegram/webhook.d.ts +2 -10
- package/dist/channels/telegram/webhook.d.ts.map +1 -1
- package/dist/channels/whatsapp/authState.d.ts +40 -9
- package/dist/channels/whatsapp/authState.d.ts.map +1 -1
- package/dist/channels/whatsapp/channel.d.ts +25 -4
- package/dist/channels/whatsapp/channel.d.ts.map +1 -1
- package/dist/channels/whatsapp/inbound.d.ts +23 -25
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.d.ts +3 -3
- package/dist/channels/whatsapp/index.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +123 -41
- package/dist/channels/whatsapp/index.mjs +120 -41
- package/dist/channels/whatsapp/pairing.d.ts.map +1 -1
- package/dist/client/src/Chat.d.ts.map +1 -1
- package/dist/client/src/ChatButton.d.ts.map +1 -1
- package/dist/client/src/browser.d.ts.map +1 -1
- package/dist/core/answer.d.ts +8 -0
- package/dist/core/answer.d.ts.map +1 -1
- package/dist/core/buildLock.d.ts +52 -0
- package/dist/core/buildLock.d.ts.map +1 -0
- package/dist/core/pipeline.d.ts +42 -5
- package/dist/core/pipeline.d.ts.map +1 -1
- package/dist/core/prompts.d.ts +2 -0
- package/dist/core/prompts.d.ts.map +1 -1
- package/dist/core/retrieval.d.ts +100 -25
- package/dist/core/retrieval.d.ts.map +1 -1
- package/dist/core/session.d.ts +8 -0
- package/dist/core/session.d.ts.map +1 -1
- package/dist/flows/index.d.ts +1 -1
- package/dist/flows/index.d.ts.map +1 -1
- package/dist/flows/index.js +8 -6
- package/dist/flows/index.mjs +7 -6
- package/dist/flows/intent.d.ts +1 -1
- package/dist/flows/intent.d.ts.map +1 -1
- package/dist/flows/loader.d.ts +1 -2
- package/dist/flows/loader.d.ts.map +1 -1
- package/dist/flows/params.d.ts +1 -1
- package/dist/flows/params.d.ts.map +1 -1
- package/dist/flows/session.d.ts +4 -2
- package/dist/flows/session.d.ts.map +1 -1
- package/dist/flows/types.d.ts +2 -3
- package/dist/flows/types.d.ts.map +1 -1
- package/dist/history/index.d.ts +1 -1
- package/dist/history/index.d.ts.map +1 -1
- package/dist/history/index.js +6 -3
- package/dist/history/index.mjs +5 -3
- package/dist/history/tursoStore.d.ts +6 -4
- package/dist/history/tursoStore.d.ts.map +1 -1
- package/dist/images/index.js +2 -2
- package/dist/images/index.mjs +2 -2
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +596 -295
- package/dist/index.mjs +595 -295
- package/dist/mcp-server/index.d.ts +5 -5
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/logger.d.ts +12 -2
- package/dist/mcp-server/logger.d.ts.map +1 -1
- package/dist/mcp-server/rate-limiter.d.ts +10 -3
- package/dist/mcp-server/rate-limiter.d.ts.map +1 -1
- package/dist/mcp-server/types.d.ts +20 -5
- package/dist/mcp-server/types.d.ts.map +1 -1
- package/dist/mcp-server.js +479 -244
- package/dist/mcp-server.mjs +485 -244
- package/dist/middleware/apiKey.d.ts +10 -0
- package/dist/middleware/apiKey.d.ts.map +1 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/ratelimit.d.ts.map +1 -1
- package/dist/middleware/referrer.d.ts.map +1 -1
- package/dist/middleware/session.d.ts +1 -1
- package/dist/middleware/session.d.ts.map +1 -1
- package/dist/routes/demo.d.ts.map +1 -1
- package/dist/routes/openai.d.ts.map +1 -1
- package/dist/routes/private.d.ts.map +1 -1
- package/dist/routes/public.d.ts.map +1 -1
- package/dist/scheduler/index.js +1 -1
- package/dist/scheduler/index.mjs +1 -1
- package/dist/scheduler/scheduler.d.ts +9 -1
- package/dist/scheduler/scheduler.d.ts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +491 -268
- package/dist/server.mjs +497 -268
- package/dist/shortener.d.ts +5 -4
- package/dist/shortener.d.ts.map +1 -1
- package/dist/types.d.ts +129 -75
- package/dist/types.d.ts.map +1 -1
- package/package.json +11 -2
package/dist/mcp-server.js
CHANGED
|
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res, err) => function __init() {
|
|
9
|
+
if (err) throw err[0];
|
|
10
|
+
try {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw err = [e], e;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
8
16
|
var __export = (target, all) => {
|
|
9
17
|
for (var name in all)
|
|
10
18
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +35,382 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
35
|
));
|
|
28
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
37
|
|
|
38
|
+
// src/core/logger.ts
|
|
39
|
+
function createConsoleLogger(level = "info") {
|
|
40
|
+
const threshold = LEVEL_ORDER[level];
|
|
41
|
+
const write = (at) => (...args) => {
|
|
42
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
43
|
+
console.error(`[${at}]`, ...args);
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
debug: write("debug"),
|
|
47
|
+
info: write("info"),
|
|
48
|
+
warn: write("warn"),
|
|
49
|
+
error: write("error")
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function resolveLogger(logger, level) {
|
|
53
|
+
return logger ?? createConsoleLogger(level);
|
|
54
|
+
}
|
|
55
|
+
var LEVEL_ORDER;
|
|
56
|
+
var init_logger = __esm({
|
|
57
|
+
"src/core/logger.ts"() {
|
|
58
|
+
"use strict";
|
|
59
|
+
LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// src/core/buildLock.ts
|
|
64
|
+
function ensureBuildLockSchema(client) {
|
|
65
|
+
let ready = schemaReady.get(client);
|
|
66
|
+
if (!ready) {
|
|
67
|
+
ready = client.execute(
|
|
68
|
+
`CREATE TABLE IF NOT EXISTS chatter_build_lock (
|
|
69
|
+
lock_key TEXT PRIMARY KEY,
|
|
70
|
+
holder_id TEXT NOT NULL,
|
|
71
|
+
heartbeat_at INTEGER NOT NULL
|
|
72
|
+
)`
|
|
73
|
+
).catch((error) => {
|
|
74
|
+
schemaReady.delete(client);
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
schemaReady.set(client, ready);
|
|
78
|
+
}
|
|
79
|
+
return ready;
|
|
80
|
+
}
|
|
81
|
+
function createTursoBuildLock(client) {
|
|
82
|
+
return {
|
|
83
|
+
async tryAcquire(key, holderId, now, staleMs) {
|
|
84
|
+
await ensureBuildLockSchema(client);
|
|
85
|
+
const result = await client.execute({
|
|
86
|
+
sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
|
|
87
|
+
ON CONFLICT(lock_key) DO UPDATE SET
|
|
88
|
+
holder_id = excluded.holder_id,
|
|
89
|
+
heartbeat_at = excluded.heartbeat_at
|
|
90
|
+
WHERE chatter_build_lock.holder_id = excluded.holder_id
|
|
91
|
+
OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
|
|
92
|
+
RETURNING holder_id`,
|
|
93
|
+
args: [key, holderId, now, staleMs]
|
|
94
|
+
});
|
|
95
|
+
const row = result.rows[0];
|
|
96
|
+
return Boolean(row) && row.holder_id === holderId;
|
|
97
|
+
},
|
|
98
|
+
async release(key, holderId) {
|
|
99
|
+
await ensureBuildLockSchema(client);
|
|
100
|
+
await client.execute({
|
|
101
|
+
sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
|
|
102
|
+
args: [key, holderId]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
|
|
108
|
+
var init_buildLock = __esm({
|
|
109
|
+
"src/core/buildLock.ts"() {
|
|
110
|
+
"use strict";
|
|
111
|
+
BUILD_LOCK_KEY = "knowledge_build";
|
|
112
|
+
BUILD_LOCK_STALE_MS = 10 * 6e4;
|
|
113
|
+
schemaReady = /* @__PURE__ */ new WeakMap();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/core/loaders.ts
|
|
118
|
+
function walk(dir) {
|
|
119
|
+
const items = [];
|
|
120
|
+
try {
|
|
121
|
+
for (const e of (0, import_node_fs2.readdirSync)(dir)) {
|
|
122
|
+
const p = (0, import_node_path2.join)(dir, e);
|
|
123
|
+
const st = (0, import_node_fs2.statSync)(p);
|
|
124
|
+
if (st.isDirectory()) items.push(...walk(p));
|
|
125
|
+
else if (p.endsWith(".md")) items.push(p);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
return items;
|
|
130
|
+
}
|
|
131
|
+
function loadKnowledge(root = "config/knowledge") {
|
|
132
|
+
const docs = [];
|
|
133
|
+
for (const bucket of ["base", "public", "private"]) {
|
|
134
|
+
const dir = (0, import_node_path2.join)(root, bucket);
|
|
135
|
+
for (const f of walk(dir)) {
|
|
136
|
+
docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return docs;
|
|
140
|
+
}
|
|
141
|
+
var import_node_fs2, import_node_path2;
|
|
142
|
+
var init_loaders = __esm({
|
|
143
|
+
"src/core/loaders.ts"() {
|
|
144
|
+
"use strict";
|
|
145
|
+
import_node_fs2 = require("node:fs");
|
|
146
|
+
import_node_path2 = require("node:path");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// src/core/retrieval.ts
|
|
151
|
+
var retrieval_exports = {};
|
|
152
|
+
__export(retrieval_exports, {
|
|
153
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
|
|
154
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
155
|
+
VectorStore: () => VectorStore,
|
|
156
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
157
|
+
openLibsqlClient: () => openLibsqlClient,
|
|
158
|
+
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
159
|
+
});
|
|
160
|
+
function createOpenAIEmbedder(client) {
|
|
161
|
+
return async (input) => {
|
|
162
|
+
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
163
|
+
return res.data.map((d) => d.embedding);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function wrapMissingLibsqlError(cause) {
|
|
167
|
+
return new Error(
|
|
168
|
+
"Chatter's default knowledge store needs the optional peer dependency '@libsql/client', which is not installed. Install it with `bun add @libsql/client` (or npm/pnpm/yarn), or set config.retriever to use your own retrieval backend instead.",
|
|
169
|
+
{ cause }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
async function openLibsqlClient(database) {
|
|
173
|
+
const mod = await import("@libsql/client").catch((error) => {
|
|
174
|
+
throw wrapMissingLibsqlError(error);
|
|
175
|
+
});
|
|
176
|
+
return mod.createClient({ url: database.url, authToken: database.authToken });
|
|
177
|
+
}
|
|
178
|
+
function chunk(text, max = 900) {
|
|
179
|
+
const out = [];
|
|
180
|
+
let buf = "";
|
|
181
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
182
|
+
if (`${buf}
|
|
183
|
+
${line}`.length > max) {
|
|
184
|
+
out.push(buf.trim());
|
|
185
|
+
buf = line;
|
|
186
|
+
} else buf += `
|
|
187
|
+
${line}`;
|
|
188
|
+
}
|
|
189
|
+
if (buf.trim()) out.push(buf.trim());
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
async function sha256(input) {
|
|
193
|
+
const data = new TextEncoder().encode(input);
|
|
194
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
195
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
196
|
+
}
|
|
197
|
+
var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
|
|
198
|
+
var init_retrieval = __esm({
|
|
199
|
+
"src/core/retrieval.ts"() {
|
|
200
|
+
"use strict";
|
|
201
|
+
init_buildLock();
|
|
202
|
+
init_loaders();
|
|
203
|
+
init_logger();
|
|
204
|
+
EMB_MODEL = "text-embedding-3-large";
|
|
205
|
+
DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
|
|
206
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE = "config.database is required unless config.retriever is set - Chatter's default knowledge store (VectorStore) needs a Turso/libsql connection. Set config.database, or supply config.retriever to use your own retrieval backend instead.";
|
|
207
|
+
VectorStore = class _VectorStore {
|
|
208
|
+
constructor(embed, options) {
|
|
209
|
+
this.embed = embed;
|
|
210
|
+
this.db = options.databaseClient;
|
|
211
|
+
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
212
|
+
this.logger = options.logger ?? createConsoleLogger();
|
|
213
|
+
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
214
|
+
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
215
|
+
this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
|
|
216
|
+
}
|
|
217
|
+
embed;
|
|
218
|
+
/**
|
|
219
|
+
* The libsql client backing this store - the same instance passed in as
|
|
220
|
+
* `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
|
|
221
|
+
* and the store share one connection.
|
|
222
|
+
*/
|
|
223
|
+
db;
|
|
224
|
+
knowledgeDir;
|
|
225
|
+
logger;
|
|
226
|
+
buildLock;
|
|
227
|
+
instanceId;
|
|
228
|
+
buildLockStaleMs;
|
|
229
|
+
/**
|
|
230
|
+
* On boot: ingest new chunks and embed only missing ones.
|
|
231
|
+
*
|
|
232
|
+
* The whole ingest runs under a single-writer lock held in the database
|
|
233
|
+
* (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
|
|
234
|
+
* every chunk id the current `knowledgeDir` did not produce. Without the
|
|
235
|
+
* lock, a second instance booting mid-build diffs against a database the
|
|
236
|
+
* first one is still writing and deletes its chunks. An instance that
|
|
237
|
+
* cannot take the lock skips its build entirely and serves what the holder
|
|
238
|
+
* has already written, rather than racing it.
|
|
239
|
+
*/
|
|
240
|
+
async build() {
|
|
241
|
+
this.logger.info("Building knowledge base...");
|
|
242
|
+
await this.db.execute(`
|
|
243
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
244
|
+
`);
|
|
245
|
+
await this.db.execute(`
|
|
246
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
247
|
+
`);
|
|
248
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
249
|
+
if (!await this.renewBuildLock()) {
|
|
250
|
+
this.logger.warn(
|
|
251
|
+
"Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await this.buildUnderLock();
|
|
257
|
+
} finally {
|
|
258
|
+
await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
|
|
262
|
+
renewBuildLock() {
|
|
263
|
+
return this.buildLock.tryAcquire(
|
|
264
|
+
BUILD_LOCK_KEY,
|
|
265
|
+
this.instanceId,
|
|
266
|
+
Date.now(),
|
|
267
|
+
this.buildLockStaleMs
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
/** The ingest itself. Only ever runs with the build lock held. */
|
|
271
|
+
async buildUnderLock() {
|
|
272
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
273
|
+
this.logger.info(`Loaded ${docs.length} knowledge documents`);
|
|
274
|
+
if (docs.length === 0) {
|
|
275
|
+
const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
|
|
276
|
+
const existingCount = Number(existing.rows[0]?.count ?? 0);
|
|
277
|
+
if (existingCount > 0) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Refusing to build: loaded 0 knowledge documents from "${this.knowledgeDir}", but ${existingCount} chunks already exist. Proceeding would delete all of them as stale. Check that knowledgeDir resolves to the right path.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const rows = [];
|
|
286
|
+
for (const d of docs) {
|
|
287
|
+
for (const part of chunk(d.text)) {
|
|
288
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
289
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
293
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
294
|
+
const UPSERT_BATCH = 500;
|
|
295
|
+
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
296
|
+
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
297
|
+
await this.db.batch(
|
|
298
|
+
batch.map((r) => ({
|
|
299
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
300
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
301
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
302
|
+
})),
|
|
303
|
+
"write"
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const ids = rows.map((r) => r.id);
|
|
307
|
+
const missing = [];
|
|
308
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
309
|
+
const batch = ids.slice(i, i + 500);
|
|
310
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
311
|
+
const res = await this.db.execute({
|
|
312
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
313
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
314
|
+
args: batch
|
|
315
|
+
});
|
|
316
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
317
|
+
}
|
|
318
|
+
if (missing.length === 0) {
|
|
319
|
+
this.logger.info("No new chunks to embed - knowledge base is up to date");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
323
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
324
|
+
const BATCH = 96;
|
|
325
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
326
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
327
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
328
|
+
const vectors = await this.embed(inputs);
|
|
329
|
+
await this.db.batch(
|
|
330
|
+
vectors.map((embedding, idx) => ({
|
|
331
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
332
|
+
args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
|
|
333
|
+
})),
|
|
334
|
+
"write"
|
|
335
|
+
);
|
|
336
|
+
if (!await this.renewBuildLock()) {
|
|
337
|
+
this.logger.warn(
|
|
338
|
+
"Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
343
|
+
}
|
|
344
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
345
|
+
async cleanupStaleChunks(currentIds) {
|
|
346
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
347
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
348
|
+
const currentIdSet = new Set(currentIds);
|
|
349
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
350
|
+
if (staleIds.length === 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
|
|
354
|
+
const BATCH_SIZE = 500;
|
|
355
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
356
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
357
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
358
|
+
await this.db.batch(
|
|
359
|
+
[
|
|
360
|
+
{ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
|
|
361
|
+
{ sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
|
|
362
|
+
],
|
|
363
|
+
"write"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
367
|
+
}
|
|
368
|
+
static cosine(a, b) {
|
|
369
|
+
let dot = 0;
|
|
370
|
+
let na = 0;
|
|
371
|
+
let nb = 0;
|
|
372
|
+
for (let i = 0; i < a.length; i++) {
|
|
373
|
+
dot += a[i] * b[i];
|
|
374
|
+
na += a[i] * a[i];
|
|
375
|
+
nb += b[i] * b[i];
|
|
376
|
+
}
|
|
377
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
381
|
+
* & portable).
|
|
382
|
+
*
|
|
383
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
384
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
385
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
386
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
387
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
388
|
+
* the embedding call.
|
|
389
|
+
*/
|
|
390
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
391
|
+
if (allowed.length === 0) return [];
|
|
392
|
+
const [qv] = await this.embed([q]);
|
|
393
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
394
|
+
const res = await this.db.execute({
|
|
395
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
396
|
+
FROM chunks c
|
|
397
|
+
JOIN embeddings e ON e.id = c.id
|
|
398
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
399
|
+
args: allowed
|
|
400
|
+
});
|
|
401
|
+
const scored = [];
|
|
402
|
+
for (const row of res.rows) {
|
|
403
|
+
const emb = JSON.parse(String(row.embedding));
|
|
404
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
405
|
+
scored.push({ s, text: String(row.text) });
|
|
406
|
+
}
|
|
407
|
+
scored.sort((a, b) => b.s - a.s);
|
|
408
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
30
414
|
// src/mcp-server.ts
|
|
31
415
|
var mcp_server_exports = {};
|
|
32
416
|
__export(mcp_server_exports, {
|
|
@@ -107,7 +491,8 @@ async function answerOnce({
|
|
|
107
491
|
async function applyTransformReply(transformReply, input, logger) {
|
|
108
492
|
if (!transformReply) return input.text;
|
|
109
493
|
try {
|
|
110
|
-
|
|
494
|
+
const result = await transformReply(input);
|
|
495
|
+
return typeof result === "string" ? result : null;
|
|
111
496
|
} catch (error) {
|
|
112
497
|
logger?.error(
|
|
113
498
|
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
@@ -148,24 +533,8 @@ async function resolveBuckets({
|
|
|
148
533
|
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
149
534
|
}
|
|
150
535
|
|
|
151
|
-
// src/
|
|
152
|
-
|
|
153
|
-
function createConsoleLogger(level = "info") {
|
|
154
|
-
const threshold = LEVEL_ORDER[level];
|
|
155
|
-
const write = (at) => (...args) => {
|
|
156
|
-
if (LEVEL_ORDER[at] < threshold) return;
|
|
157
|
-
console.error(`[${at}]`, ...args);
|
|
158
|
-
};
|
|
159
|
-
return {
|
|
160
|
-
debug: write("debug"),
|
|
161
|
-
info: write("info"),
|
|
162
|
-
warn: write("warn"),
|
|
163
|
-
error: write("error")
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
function resolveLogger(logger, level) {
|
|
167
|
-
return logger ?? createConsoleLogger(level);
|
|
168
|
-
}
|
|
536
|
+
// src/mcp-server.ts
|
|
537
|
+
init_logger();
|
|
169
538
|
|
|
170
539
|
// src/core/messages.ts
|
|
171
540
|
function lastUserMessage(messages) {
|
|
@@ -188,6 +557,7 @@ async function prepareChat({
|
|
|
188
557
|
sender,
|
|
189
558
|
rewriteQuery,
|
|
190
559
|
rerankContext,
|
|
560
|
+
fallbackFn,
|
|
191
561
|
logger
|
|
192
562
|
}) {
|
|
193
563
|
const lastUserMsg = lastUserMessage(messages);
|
|
@@ -204,21 +574,39 @@ async function prepareChat({
|
|
|
204
574
|
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
205
575
|
}
|
|
206
576
|
}
|
|
207
|
-
|
|
577
|
+
const resolvedBuckets = buckets ?? defaultBuckets(mode);
|
|
578
|
+
const retrievedChunks = await store.query(query, topK, resolvedBuckets);
|
|
579
|
+
let ctx = retrievedChunks;
|
|
208
580
|
if (rerankContext) {
|
|
209
581
|
try {
|
|
210
|
-
const reranked = await rerankContext({ query, chunks: ctx });
|
|
582
|
+
const reranked = await rerankContext({ query, chunks: [...ctx] });
|
|
211
583
|
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
212
584
|
} catch (error) {
|
|
213
585
|
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
214
586
|
}
|
|
215
587
|
}
|
|
588
|
+
let fallback;
|
|
589
|
+
if (ctx.length === 0 && fallbackFn) {
|
|
590
|
+
try {
|
|
591
|
+
const guidance = await fallbackFn({
|
|
592
|
+
query,
|
|
593
|
+
mode,
|
|
594
|
+
buckets: resolvedBuckets,
|
|
595
|
+
sender,
|
|
596
|
+
retrievedChunks
|
|
597
|
+
});
|
|
598
|
+
if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
|
|
599
|
+
} catch (error) {
|
|
600
|
+
logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
216
603
|
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
217
604
|
const hint = channelHint?.trim();
|
|
218
605
|
const system = [
|
|
219
606
|
prompts.baseSystemRules,
|
|
220
607
|
persona,
|
|
221
608
|
...hint ? [hint] : [],
|
|
609
|
+
...fallback ? [fallback] : [],
|
|
222
610
|
`${label}:
|
|
223
611
|
${ctx.join("\n\n")}`
|
|
224
612
|
].join("\n\n");
|
|
@@ -228,6 +616,7 @@ ${ctx.join("\n\n")}`
|
|
|
228
616
|
// src/core/prompts.ts
|
|
229
617
|
var import_node_fs = require("node:fs");
|
|
230
618
|
var import_node_path = require("node:path");
|
|
619
|
+
var DEFAULT_PROMPTS_DIR = "./config/prompts";
|
|
231
620
|
var PromptLoader = class {
|
|
232
621
|
constructor(promptsDir, bot) {
|
|
233
622
|
this.promptsDir = promptsDir;
|
|
@@ -258,206 +647,8 @@ var PromptLoader = class {
|
|
|
258
647
|
}
|
|
259
648
|
};
|
|
260
649
|
|
|
261
|
-
// src/
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// src/core/loaders.ts
|
|
265
|
-
var import_node_fs2 = require("node:fs");
|
|
266
|
-
var import_node_path2 = require("node:path");
|
|
267
|
-
function walk(dir) {
|
|
268
|
-
const items = [];
|
|
269
|
-
try {
|
|
270
|
-
for (const e of (0, import_node_fs2.readdirSync)(dir)) {
|
|
271
|
-
const p = (0, import_node_path2.join)(dir, e);
|
|
272
|
-
const st = (0, import_node_fs2.statSync)(p);
|
|
273
|
-
if (st.isDirectory()) items.push(...walk(p));
|
|
274
|
-
else if (p.endsWith(".md")) items.push(p);
|
|
275
|
-
}
|
|
276
|
-
} catch {
|
|
277
|
-
}
|
|
278
|
-
return items;
|
|
279
|
-
}
|
|
280
|
-
function loadKnowledge(root = "config/knowledge") {
|
|
281
|
-
const docs = [];
|
|
282
|
-
for (const bucket of ["base", "public", "private"]) {
|
|
283
|
-
const dir = (0, import_node_path2.join)(root, bucket);
|
|
284
|
-
for (const f of walk(dir)) {
|
|
285
|
-
docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
return docs;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// src/core/retrieval.ts
|
|
292
|
-
var EMB_MODEL = "text-embedding-3-large";
|
|
293
|
-
function chunk(text, max = 900) {
|
|
294
|
-
const out = [];
|
|
295
|
-
let buf = "";
|
|
296
|
-
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
297
|
-
if (`${buf}
|
|
298
|
-
${line}`.length > max) {
|
|
299
|
-
out.push(buf.trim());
|
|
300
|
-
buf = line;
|
|
301
|
-
} else buf += `
|
|
302
|
-
${line}`;
|
|
303
|
-
}
|
|
304
|
-
if (buf.trim()) out.push(buf.trim());
|
|
305
|
-
return out;
|
|
306
|
-
}
|
|
307
|
-
async function sha256(input) {
|
|
308
|
-
const data = new TextEncoder().encode(input);
|
|
309
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
310
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
311
|
-
}
|
|
312
|
-
var VectorStore = class _VectorStore {
|
|
313
|
-
constructor(client, options) {
|
|
314
|
-
this.client = client;
|
|
315
|
-
this.db = options.databaseClient ? options.databaseClient : (0, import_client.createClient)({
|
|
316
|
-
url: options.databaseUrl,
|
|
317
|
-
authToken: options.databaseAuthToken
|
|
318
|
-
});
|
|
319
|
-
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
320
|
-
this.logger = options.logger ?? createConsoleLogger();
|
|
321
|
-
}
|
|
322
|
-
client;
|
|
323
|
-
/**
|
|
324
|
-
* The libsql client backing this store. When a client was injected this is
|
|
325
|
-
* that same instance, so callers holding it (e.g. `ServerDependencies.db`)
|
|
326
|
-
* and the store share one connection.
|
|
327
|
-
*/
|
|
328
|
-
db;
|
|
329
|
-
knowledgeDir;
|
|
330
|
-
logger;
|
|
331
|
-
// On boot: ingest new chunks and embed only missing ones.
|
|
332
|
-
async build() {
|
|
333
|
-
this.logger.info("\u{1F504} Building knowledge base...");
|
|
334
|
-
await this.db.execute(`
|
|
335
|
-
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
336
|
-
`);
|
|
337
|
-
await this.db.execute(`
|
|
338
|
-
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
339
|
-
`);
|
|
340
|
-
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
341
|
-
const docs = loadKnowledge(this.knowledgeDir);
|
|
342
|
-
this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
343
|
-
const rows = [];
|
|
344
|
-
for (const d of docs) {
|
|
345
|
-
for (const part of chunk(d.text)) {
|
|
346
|
-
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
347
|
-
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
351
|
-
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
352
|
-
const UPSERT_BATCH = 500;
|
|
353
|
-
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
354
|
-
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
355
|
-
await this.db.batch(
|
|
356
|
-
batch.map((r) => ({
|
|
357
|
-
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
358
|
-
ON CONFLICT(id) DO NOTHING`,
|
|
359
|
-
args: [r.id, r.bucket, r.source, r.text]
|
|
360
|
-
})),
|
|
361
|
-
"write"
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
const ids = rows.map((r) => r.id);
|
|
365
|
-
const missing = [];
|
|
366
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
367
|
-
const batch = ids.slice(i, i + 500);
|
|
368
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
369
|
-
const res = await this.db.execute({
|
|
370
|
-
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
371
|
-
EXCEPT SELECT id FROM embeddings`,
|
|
372
|
-
args: batch
|
|
373
|
-
});
|
|
374
|
-
for (const row of res.rows) missing.push(String(row.id));
|
|
375
|
-
}
|
|
376
|
-
if (missing.length === 0) {
|
|
377
|
-
this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
381
|
-
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
382
|
-
const BATCH = 96;
|
|
383
|
-
for (let i = 0; i < missing.length; i += BATCH) {
|
|
384
|
-
const batchIds = missing.slice(i, i + BATCH);
|
|
385
|
-
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
386
|
-
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
387
|
-
await this.db.batch(
|
|
388
|
-
emb.data.map((d, idx) => ({
|
|
389
|
-
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
390
|
-
args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
|
|
391
|
-
})),
|
|
392
|
-
"write"
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
396
|
-
}
|
|
397
|
-
// Remove chunks from database that no longer exist in markdown files
|
|
398
|
-
async cleanupStaleChunks(currentIds) {
|
|
399
|
-
const result = await this.db.execute("SELECT id FROM chunks");
|
|
400
|
-
const dbIds = result.rows.map((row) => String(row.id));
|
|
401
|
-
const currentIdSet = new Set(currentIds);
|
|
402
|
-
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
403
|
-
if (staleIds.length === 0) {
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
407
|
-
const BATCH_SIZE = 500;
|
|
408
|
-
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
409
|
-
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
410
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
411
|
-
await this.db.execute({
|
|
412
|
-
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
413
|
-
args: batch
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
417
|
-
}
|
|
418
|
-
static cosine(a, b) {
|
|
419
|
-
let dot = 0;
|
|
420
|
-
let na = 0;
|
|
421
|
-
let nb = 0;
|
|
422
|
-
for (let i = 0; i < a.length; i++) {
|
|
423
|
-
dot += a[i] * b[i];
|
|
424
|
-
na += a[i] * a[i];
|
|
425
|
-
nb += b[i] * b[i];
|
|
426
|
-
}
|
|
427
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
431
|
-
* & portable).
|
|
432
|
-
*
|
|
433
|
-
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
434
|
-
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
435
|
-
* buckets can query them (`build` only writes and prunes the three the
|
|
436
|
-
* knowledge loader walks). Names are bound as query parameters, never
|
|
437
|
-
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
438
|
-
* the embedding call.
|
|
439
|
-
*/
|
|
440
|
-
async query(q, k = 6, allowed = ["base"]) {
|
|
441
|
-
if (allowed.length === 0) return [];
|
|
442
|
-
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
443
|
-
const placeholders = allowed.map(() => "?").join(",");
|
|
444
|
-
const res = await this.db.execute({
|
|
445
|
-
sql: `SELECT c.id, c.text, e.embedding
|
|
446
|
-
FROM chunks c
|
|
447
|
-
JOIN embeddings e ON e.id = c.id
|
|
448
|
-
WHERE c.bucket IN (${placeholders})`,
|
|
449
|
-
args: allowed
|
|
450
|
-
});
|
|
451
|
-
const scored = [];
|
|
452
|
-
for (const row of res.rows) {
|
|
453
|
-
const emb = JSON.parse(String(row.embedding));
|
|
454
|
-
const s = _VectorStore.cosine(qv, emb);
|
|
455
|
-
scored.push({ s, text: String(row.text) });
|
|
456
|
-
}
|
|
457
|
-
scored.sort((a, b) => b.s - a.s);
|
|
458
|
-
return scored.slice(0, k).map((r) => r.text);
|
|
459
|
-
}
|
|
460
|
-
};
|
|
650
|
+
// src/mcp-server.ts
|
|
651
|
+
init_retrieval();
|
|
461
652
|
|
|
462
653
|
// src/mcp-server/conversation-id.ts
|
|
463
654
|
function generateConversationId() {
|
|
@@ -484,28 +675,38 @@ function calculateCost(usage, rates) {
|
|
|
484
675
|
}
|
|
485
676
|
|
|
486
677
|
// src/mcp-server/logger.ts
|
|
678
|
+
init_logger();
|
|
487
679
|
var MCPLogger = class {
|
|
488
680
|
enableConsole;
|
|
489
681
|
callback;
|
|
490
682
|
logger;
|
|
491
|
-
|
|
683
|
+
logContent;
|
|
684
|
+
constructor(enableConsole = true, callback, logger, logContent = false) {
|
|
492
685
|
this.enableConsole = enableConsole;
|
|
493
686
|
this.callback = callback;
|
|
494
687
|
this.logger = logger ?? createConsoleLogger();
|
|
688
|
+
this.logContent = logContent;
|
|
495
689
|
}
|
|
496
690
|
/**
|
|
497
691
|
* Log a chat interaction
|
|
498
692
|
*/
|
|
499
693
|
async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
|
|
500
694
|
const lastUserMsg = lastUserMessage(conversationMessages);
|
|
695
|
+
const content = {
|
|
696
|
+
userMessage: scrubOutput(lastUserMsg?.content || ""),
|
|
697
|
+
conversationHistory: conversationMessages.map((msg) => ({
|
|
698
|
+
...msg,
|
|
699
|
+
content: scrubOutput(msg.content)
|
|
700
|
+
})),
|
|
701
|
+
ragContext: ragContext.map(scrubOutput),
|
|
702
|
+
response: scrubOutput(response)
|
|
703
|
+
};
|
|
704
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
501
705
|
const logEvent = {
|
|
502
|
-
timestamp
|
|
706
|
+
timestamp,
|
|
503
707
|
toolName,
|
|
504
708
|
conversationId,
|
|
505
|
-
|
|
506
|
-
conversationHistory: conversationMessages,
|
|
507
|
-
ragContext,
|
|
508
|
-
response,
|
|
709
|
+
...content,
|
|
509
710
|
duration,
|
|
510
711
|
cost
|
|
511
712
|
};
|
|
@@ -513,9 +714,27 @@ var MCPLogger = class {
|
|
|
513
714
|
this.logger.info(
|
|
514
715
|
JSON.stringify({
|
|
515
716
|
event: "mcp_chat",
|
|
516
|
-
|
|
717
|
+
timestamp,
|
|
718
|
+
toolName,
|
|
719
|
+
conversationId,
|
|
720
|
+
messageCount: conversationMessages.length,
|
|
721
|
+
ragContextCount: ragContext.length,
|
|
722
|
+
responseChars: response.length,
|
|
723
|
+
duration,
|
|
724
|
+
cost
|
|
517
725
|
})
|
|
518
726
|
);
|
|
727
|
+
if (this.logContent) {
|
|
728
|
+
this.logger.debug(
|
|
729
|
+
JSON.stringify({
|
|
730
|
+
event: "mcp_chat_content",
|
|
731
|
+
timestamp,
|
|
732
|
+
toolName,
|
|
733
|
+
conversationId,
|
|
734
|
+
...content
|
|
735
|
+
})
|
|
736
|
+
);
|
|
737
|
+
}
|
|
519
738
|
}
|
|
520
739
|
if (this.callback) {
|
|
521
740
|
try {
|
|
@@ -526,11 +745,12 @@ var MCPLogger = class {
|
|
|
526
745
|
}
|
|
527
746
|
}
|
|
528
747
|
};
|
|
529
|
-
function createLogger(enableConsole = true, callback, logger) {
|
|
530
|
-
return new MCPLogger(enableConsole, callback, logger);
|
|
748
|
+
function createLogger(enableConsole = true, callback, logger, logContent = false) {
|
|
749
|
+
return new MCPLogger(enableConsole, callback, logger, logContent);
|
|
531
750
|
}
|
|
532
751
|
|
|
533
752
|
// src/mcp-server/rate-limiter.ts
|
|
753
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
534
754
|
var RateLimiter = class {
|
|
535
755
|
requestMap;
|
|
536
756
|
limit;
|
|
@@ -539,9 +759,9 @@ var RateLimiter = class {
|
|
|
539
759
|
* Create a new rate limiter
|
|
540
760
|
*
|
|
541
761
|
* @param limit - Maximum requests allowed in the time window
|
|
542
|
-
* @param windowMs - Time window in milliseconds (default:
|
|
762
|
+
* @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
|
|
543
763
|
*/
|
|
544
|
-
constructor(limit, windowMs =
|
|
764
|
+
constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
545
765
|
this.requestMap = /* @__PURE__ */ new Map();
|
|
546
766
|
this.limit = limit;
|
|
547
767
|
this.windowMs = windowMs;
|
|
@@ -605,7 +825,7 @@ var RateLimiter = class {
|
|
|
605
825
|
return Math.max(0, timeUntilReset);
|
|
606
826
|
}
|
|
607
827
|
};
|
|
608
|
-
function
|
|
828
|
+
function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
609
829
|
return limit ? new RateLimiter(limit, windowMs) : null;
|
|
610
830
|
}
|
|
611
831
|
|
|
@@ -655,11 +875,15 @@ async function createMCPServer(config) {
|
|
|
655
875
|
const log = resolveLogger(config.logger, config.logLevel);
|
|
656
876
|
log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
|
|
657
877
|
const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
|
|
658
|
-
const
|
|
659
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
878
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
660
879
|
const transportMode = config.transport || "stdio";
|
|
661
|
-
const rateLimiter =
|
|
662
|
-
const logger = createLogger(
|
|
880
|
+
const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
|
|
881
|
+
const logger = createLogger(
|
|
882
|
+
config.logging?.console !== false,
|
|
883
|
+
config.logging?.onChat,
|
|
884
|
+
log,
|
|
885
|
+
config.logging?.content === true
|
|
886
|
+
);
|
|
663
887
|
const publicToolConfig = {
|
|
664
888
|
enabled: config.tools?.public?.enabled !== false,
|
|
665
889
|
name: config.tools?.public?.name || "chat_public",
|
|
@@ -674,13 +898,22 @@ async function createMCPServer(config) {
|
|
|
674
898
|
apiKey: config.openai.apiKey
|
|
675
899
|
});
|
|
676
900
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
901
|
+
let store;
|
|
902
|
+
if (config.retriever) {
|
|
903
|
+
store = config.retriever;
|
|
904
|
+
} else {
|
|
905
|
+
if (!config.database) {
|
|
906
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
907
|
+
}
|
|
908
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
909
|
+
const db = await openLibsqlClient2(config.database);
|
|
910
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
911
|
+
databaseClient: db,
|
|
912
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
913
|
+
logger: log
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
await store.build?.();
|
|
684
917
|
log.info("\u2705 Knowledge base ready");
|
|
685
918
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
686
919
|
const server = new McpServer({
|
|
@@ -729,6 +962,7 @@ async function createMCPServer(config) {
|
|
|
729
962
|
buckets,
|
|
730
963
|
rewriteQuery: config.rewriteQuery,
|
|
731
964
|
rerankContext: config.rerankContext,
|
|
965
|
+
fallbackFn: config.fallbackFn,
|
|
732
966
|
logger: log
|
|
733
967
|
});
|
|
734
968
|
const result = await answerOnce({
|
|
@@ -818,6 +1052,7 @@ async function createMCPServer(config) {
|
|
|
818
1052
|
buckets,
|
|
819
1053
|
rewriteQuery: config.rewriteQuery,
|
|
820
1054
|
rerankContext: config.rerankContext,
|
|
1055
|
+
fallbackFn: config.fallbackFn,
|
|
821
1056
|
logger: log
|
|
822
1057
|
});
|
|
823
1058
|
const result = await answerOnce({
|