@diegoaltoworks/chatter 0.61.0 → 0.63.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 +55 -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 +25 -6
- package/dist/channels/matrix/api.d.ts.map +1 -1
- package/dist/channels/matrix/channel.d.ts +4 -56
- package/dist/channels/matrix/channel.d.ts.map +1 -1
- package/dist/channels/matrix/handler.d.ts +16 -9
- package/dist/channels/matrix/handler.d.ts.map +1 -1
- package/dist/channels/matrix/index.d.ts +2 -2
- package/dist/channels/matrix/index.d.ts.map +1 -1
- package/dist/channels/matrix/index.js +196 -40
- package/dist/channels/matrix/index.mjs +189 -39
- package/dist/channels/matrix/sync.d.ts +4 -1
- package/dist/channels/matrix/sync.d.ts.map +1 -1
- package/dist/channels/matrix/updates.d.ts +39 -10
- package/dist/channels/matrix/updates.d.ts.map +1 -1
- 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 +39 -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 +188 -66
- package/dist/channels/whatsapp/index.mjs +185 -66
- 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 +6 -3
package/dist/mcp-server.mjs
CHANGED
|
@@ -1,3 +1,393 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res, err) => function __init() {
|
|
4
|
+
if (err) throw err[0];
|
|
5
|
+
try {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw err = [e], e;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/core/logger.ts
|
|
17
|
+
function createConsoleLogger(level = "info") {
|
|
18
|
+
const threshold = LEVEL_ORDER[level];
|
|
19
|
+
const write = (at) => (...args) => {
|
|
20
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
21
|
+
console.error(`[${at}]`, ...args);
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
debug: write("debug"),
|
|
25
|
+
info: write("info"),
|
|
26
|
+
warn: write("warn"),
|
|
27
|
+
error: write("error")
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function resolveLogger(logger, level) {
|
|
31
|
+
return logger ?? createConsoleLogger(level);
|
|
32
|
+
}
|
|
33
|
+
var LEVEL_ORDER;
|
|
34
|
+
var init_logger = __esm({
|
|
35
|
+
"src/core/logger.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// src/core/buildLock.ts
|
|
42
|
+
function ensureBuildLockSchema(client) {
|
|
43
|
+
let ready = schemaReady.get(client);
|
|
44
|
+
if (!ready) {
|
|
45
|
+
ready = client.execute(
|
|
46
|
+
`CREATE TABLE IF NOT EXISTS chatter_build_lock (
|
|
47
|
+
lock_key TEXT PRIMARY KEY,
|
|
48
|
+
holder_id TEXT NOT NULL,
|
|
49
|
+
heartbeat_at INTEGER NOT NULL
|
|
50
|
+
)`
|
|
51
|
+
).catch((error) => {
|
|
52
|
+
schemaReady.delete(client);
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
schemaReady.set(client, ready);
|
|
56
|
+
}
|
|
57
|
+
return ready;
|
|
58
|
+
}
|
|
59
|
+
function createTursoBuildLock(client) {
|
|
60
|
+
return {
|
|
61
|
+
async tryAcquire(key, holderId, now, staleMs) {
|
|
62
|
+
await ensureBuildLockSchema(client);
|
|
63
|
+
const result = await client.execute({
|
|
64
|
+
sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
|
|
65
|
+
ON CONFLICT(lock_key) DO UPDATE SET
|
|
66
|
+
holder_id = excluded.holder_id,
|
|
67
|
+
heartbeat_at = excluded.heartbeat_at
|
|
68
|
+
WHERE chatter_build_lock.holder_id = excluded.holder_id
|
|
69
|
+
OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
|
|
70
|
+
RETURNING holder_id`,
|
|
71
|
+
args: [key, holderId, now, staleMs]
|
|
72
|
+
});
|
|
73
|
+
const row = result.rows[0];
|
|
74
|
+
return Boolean(row) && row.holder_id === holderId;
|
|
75
|
+
},
|
|
76
|
+
async release(key, holderId) {
|
|
77
|
+
await ensureBuildLockSchema(client);
|
|
78
|
+
await client.execute({
|
|
79
|
+
sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
|
|
80
|
+
args: [key, holderId]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
|
|
86
|
+
var init_buildLock = __esm({
|
|
87
|
+
"src/core/buildLock.ts"() {
|
|
88
|
+
"use strict";
|
|
89
|
+
BUILD_LOCK_KEY = "knowledge_build";
|
|
90
|
+
BUILD_LOCK_STALE_MS = 10 * 6e4;
|
|
91
|
+
schemaReady = /* @__PURE__ */ new WeakMap();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// src/core/loaders.ts
|
|
96
|
+
import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
97
|
+
import { join as join2 } from "node:path";
|
|
98
|
+
function walk(dir) {
|
|
99
|
+
const items = [];
|
|
100
|
+
try {
|
|
101
|
+
for (const e of readdirSync(dir)) {
|
|
102
|
+
const p = join2(dir, e);
|
|
103
|
+
const st = statSync(p);
|
|
104
|
+
if (st.isDirectory()) items.push(...walk(p));
|
|
105
|
+
else if (p.endsWith(".md")) items.push(p);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
function loadKnowledge(root = "config/knowledge") {
|
|
112
|
+
const docs = [];
|
|
113
|
+
for (const bucket of ["base", "public", "private"]) {
|
|
114
|
+
const dir = join2(root, bucket);
|
|
115
|
+
for (const f of walk(dir)) {
|
|
116
|
+
docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return docs;
|
|
120
|
+
}
|
|
121
|
+
var init_loaders = __esm({
|
|
122
|
+
"src/core/loaders.ts"() {
|
|
123
|
+
"use strict";
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/core/retrieval.ts
|
|
128
|
+
var retrieval_exports = {};
|
|
129
|
+
__export(retrieval_exports, {
|
|
130
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
|
|
131
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
132
|
+
VectorStore: () => VectorStore,
|
|
133
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
134
|
+
openLibsqlClient: () => openLibsqlClient,
|
|
135
|
+
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
136
|
+
});
|
|
137
|
+
function createOpenAIEmbedder(client) {
|
|
138
|
+
return async (input) => {
|
|
139
|
+
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
140
|
+
return res.data.map((d) => d.embedding);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function wrapMissingLibsqlError(cause) {
|
|
144
|
+
return new Error(
|
|
145
|
+
"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.",
|
|
146
|
+
{ cause }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
async function openLibsqlClient(database) {
|
|
150
|
+
const mod = await import("@libsql/client").catch((error) => {
|
|
151
|
+
throw wrapMissingLibsqlError(error);
|
|
152
|
+
});
|
|
153
|
+
return mod.createClient({ url: database.url, authToken: database.authToken });
|
|
154
|
+
}
|
|
155
|
+
function chunk(text, max = 900) {
|
|
156
|
+
const out = [];
|
|
157
|
+
let buf = "";
|
|
158
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
159
|
+
if (`${buf}
|
|
160
|
+
${line}`.length > max) {
|
|
161
|
+
out.push(buf.trim());
|
|
162
|
+
buf = line;
|
|
163
|
+
} else buf += `
|
|
164
|
+
${line}`;
|
|
165
|
+
}
|
|
166
|
+
if (buf.trim()) out.push(buf.trim());
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
async function sha256(input) {
|
|
170
|
+
const data = new TextEncoder().encode(input);
|
|
171
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
172
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
173
|
+
}
|
|
174
|
+
var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
|
|
175
|
+
var init_retrieval = __esm({
|
|
176
|
+
"src/core/retrieval.ts"() {
|
|
177
|
+
"use strict";
|
|
178
|
+
init_buildLock();
|
|
179
|
+
init_loaders();
|
|
180
|
+
init_logger();
|
|
181
|
+
EMB_MODEL = "text-embedding-3-large";
|
|
182
|
+
DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
|
|
183
|
+
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.";
|
|
184
|
+
VectorStore = class _VectorStore {
|
|
185
|
+
constructor(embed, options) {
|
|
186
|
+
this.embed = embed;
|
|
187
|
+
this.db = options.databaseClient;
|
|
188
|
+
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
189
|
+
this.logger = options.logger ?? createConsoleLogger();
|
|
190
|
+
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
191
|
+
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
192
|
+
this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
|
|
193
|
+
}
|
|
194
|
+
embed;
|
|
195
|
+
/**
|
|
196
|
+
* The libsql client backing this store - the same instance passed in as
|
|
197
|
+
* `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
|
|
198
|
+
* and the store share one connection.
|
|
199
|
+
*/
|
|
200
|
+
db;
|
|
201
|
+
knowledgeDir;
|
|
202
|
+
logger;
|
|
203
|
+
buildLock;
|
|
204
|
+
instanceId;
|
|
205
|
+
buildLockStaleMs;
|
|
206
|
+
/**
|
|
207
|
+
* On boot: ingest new chunks and embed only missing ones.
|
|
208
|
+
*
|
|
209
|
+
* The whole ingest runs under a single-writer lock held in the database
|
|
210
|
+
* (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
|
|
211
|
+
* every chunk id the current `knowledgeDir` did not produce. Without the
|
|
212
|
+
* lock, a second instance booting mid-build diffs against a database the
|
|
213
|
+
* first one is still writing and deletes its chunks. An instance that
|
|
214
|
+
* cannot take the lock skips its build entirely and serves what the holder
|
|
215
|
+
* has already written, rather than racing it.
|
|
216
|
+
*/
|
|
217
|
+
async build() {
|
|
218
|
+
this.logger.info("Building knowledge base...");
|
|
219
|
+
await this.db.execute(`
|
|
220
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
221
|
+
`);
|
|
222
|
+
await this.db.execute(`
|
|
223
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
224
|
+
`);
|
|
225
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
226
|
+
if (!await this.renewBuildLock()) {
|
|
227
|
+
this.logger.warn(
|
|
228
|
+
"Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await this.buildUnderLock();
|
|
234
|
+
} finally {
|
|
235
|
+
await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
|
|
239
|
+
renewBuildLock() {
|
|
240
|
+
return this.buildLock.tryAcquire(
|
|
241
|
+
BUILD_LOCK_KEY,
|
|
242
|
+
this.instanceId,
|
|
243
|
+
Date.now(),
|
|
244
|
+
this.buildLockStaleMs
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
/** The ingest itself. Only ever runs with the build lock held. */
|
|
248
|
+
async buildUnderLock() {
|
|
249
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
250
|
+
this.logger.info(`Loaded ${docs.length} knowledge documents`);
|
|
251
|
+
if (docs.length === 0) {
|
|
252
|
+
const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
|
|
253
|
+
const existingCount = Number(existing.rows[0]?.count ?? 0);
|
|
254
|
+
if (existingCount > 0) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`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.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const rows = [];
|
|
263
|
+
for (const d of docs) {
|
|
264
|
+
for (const part of chunk(d.text)) {
|
|
265
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
266
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
270
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
271
|
+
const UPSERT_BATCH = 500;
|
|
272
|
+
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
273
|
+
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
274
|
+
await this.db.batch(
|
|
275
|
+
batch.map((r) => ({
|
|
276
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
277
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
278
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
279
|
+
})),
|
|
280
|
+
"write"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const ids = rows.map((r) => r.id);
|
|
284
|
+
const missing = [];
|
|
285
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
286
|
+
const batch = ids.slice(i, i + 500);
|
|
287
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
288
|
+
const res = await this.db.execute({
|
|
289
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
290
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
291
|
+
args: batch
|
|
292
|
+
});
|
|
293
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
294
|
+
}
|
|
295
|
+
if (missing.length === 0) {
|
|
296
|
+
this.logger.info("No new chunks to embed - knowledge base is up to date");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
300
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
301
|
+
const BATCH = 96;
|
|
302
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
303
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
304
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
305
|
+
const vectors = await this.embed(inputs);
|
|
306
|
+
await this.db.batch(
|
|
307
|
+
vectors.map((embedding, idx) => ({
|
|
308
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
309
|
+
args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
|
|
310
|
+
})),
|
|
311
|
+
"write"
|
|
312
|
+
);
|
|
313
|
+
if (!await this.renewBuildLock()) {
|
|
314
|
+
this.logger.warn(
|
|
315
|
+
"Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
320
|
+
}
|
|
321
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
322
|
+
async cleanupStaleChunks(currentIds) {
|
|
323
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
324
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
325
|
+
const currentIdSet = new Set(currentIds);
|
|
326
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
327
|
+
if (staleIds.length === 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
|
|
331
|
+
const BATCH_SIZE = 500;
|
|
332
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
333
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
334
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
335
|
+
await this.db.batch(
|
|
336
|
+
[
|
|
337
|
+
{ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
|
|
338
|
+
{ sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
|
|
339
|
+
],
|
|
340
|
+
"write"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
344
|
+
}
|
|
345
|
+
static cosine(a, b) {
|
|
346
|
+
let dot = 0;
|
|
347
|
+
let na = 0;
|
|
348
|
+
let nb = 0;
|
|
349
|
+
for (let i = 0; i < a.length; i++) {
|
|
350
|
+
dot += a[i] * b[i];
|
|
351
|
+
na += a[i] * a[i];
|
|
352
|
+
nb += b[i] * b[i];
|
|
353
|
+
}
|
|
354
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
358
|
+
* & portable).
|
|
359
|
+
*
|
|
360
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
361
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
362
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
363
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
364
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
365
|
+
* the embedding call.
|
|
366
|
+
*/
|
|
367
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
368
|
+
if (allowed.length === 0) return [];
|
|
369
|
+
const [qv] = await this.embed([q]);
|
|
370
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
371
|
+
const res = await this.db.execute({
|
|
372
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
373
|
+
FROM chunks c
|
|
374
|
+
JOIN embeddings e ON e.id = c.id
|
|
375
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
376
|
+
args: allowed
|
|
377
|
+
});
|
|
378
|
+
const scored = [];
|
|
379
|
+
for (const row of res.rows) {
|
|
380
|
+
const emb = JSON.parse(String(row.embedding));
|
|
381
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
382
|
+
scored.push({ s, text: String(row.text) });
|
|
383
|
+
}
|
|
384
|
+
scored.sort((a, b) => b.s - a.s);
|
|
385
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
1
391
|
// src/mcp-server.ts
|
|
2
392
|
import OpenAI from "openai";
|
|
3
393
|
|
|
@@ -73,7 +463,8 @@ async function answerOnce({
|
|
|
73
463
|
async function applyTransformReply(transformReply, input, logger) {
|
|
74
464
|
if (!transformReply) return input.text;
|
|
75
465
|
try {
|
|
76
|
-
|
|
466
|
+
const result = await transformReply(input);
|
|
467
|
+
return typeof result === "string" ? result : null;
|
|
77
468
|
} catch (error) {
|
|
78
469
|
logger?.error(
|
|
79
470
|
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
@@ -114,24 +505,8 @@ async function resolveBuckets({
|
|
|
114
505
|
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
115
506
|
}
|
|
116
507
|
|
|
117
|
-
// src/
|
|
118
|
-
|
|
119
|
-
function createConsoleLogger(level = "info") {
|
|
120
|
-
const threshold = LEVEL_ORDER[level];
|
|
121
|
-
const write = (at) => (...args) => {
|
|
122
|
-
if (LEVEL_ORDER[at] < threshold) return;
|
|
123
|
-
console.error(`[${at}]`, ...args);
|
|
124
|
-
};
|
|
125
|
-
return {
|
|
126
|
-
debug: write("debug"),
|
|
127
|
-
info: write("info"),
|
|
128
|
-
warn: write("warn"),
|
|
129
|
-
error: write("error")
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
function resolveLogger(logger, level) {
|
|
133
|
-
return logger ?? createConsoleLogger(level);
|
|
134
|
-
}
|
|
508
|
+
// src/mcp-server.ts
|
|
509
|
+
init_logger();
|
|
135
510
|
|
|
136
511
|
// src/core/messages.ts
|
|
137
512
|
function lastUserMessage(messages) {
|
|
@@ -154,6 +529,7 @@ async function prepareChat({
|
|
|
154
529
|
sender,
|
|
155
530
|
rewriteQuery,
|
|
156
531
|
rerankContext,
|
|
532
|
+
fallbackFn,
|
|
157
533
|
logger
|
|
158
534
|
}) {
|
|
159
535
|
const lastUserMsg = lastUserMessage(messages);
|
|
@@ -170,21 +546,39 @@ async function prepareChat({
|
|
|
170
546
|
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
171
547
|
}
|
|
172
548
|
}
|
|
173
|
-
|
|
549
|
+
const resolvedBuckets = buckets ?? defaultBuckets(mode);
|
|
550
|
+
const retrievedChunks = await store.query(query, topK, resolvedBuckets);
|
|
551
|
+
let ctx = retrievedChunks;
|
|
174
552
|
if (rerankContext) {
|
|
175
553
|
try {
|
|
176
|
-
const reranked = await rerankContext({ query, chunks: ctx });
|
|
554
|
+
const reranked = await rerankContext({ query, chunks: [...ctx] });
|
|
177
555
|
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
178
556
|
} catch (error) {
|
|
179
557
|
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
180
558
|
}
|
|
181
559
|
}
|
|
560
|
+
let fallback;
|
|
561
|
+
if (ctx.length === 0 && fallbackFn) {
|
|
562
|
+
try {
|
|
563
|
+
const guidance = await fallbackFn({
|
|
564
|
+
query,
|
|
565
|
+
mode,
|
|
566
|
+
buckets: resolvedBuckets,
|
|
567
|
+
sender,
|
|
568
|
+
retrievedChunks
|
|
569
|
+
});
|
|
570
|
+
if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
|
|
571
|
+
} catch (error) {
|
|
572
|
+
logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
182
575
|
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
183
576
|
const hint = channelHint?.trim();
|
|
184
577
|
const system = [
|
|
185
578
|
prompts.baseSystemRules,
|
|
186
579
|
persona,
|
|
187
580
|
...hint ? [hint] : [],
|
|
581
|
+
...fallback ? [fallback] : [],
|
|
188
582
|
`${label}:
|
|
189
583
|
${ctx.join("\n\n")}`
|
|
190
584
|
].join("\n\n");
|
|
@@ -194,6 +588,7 @@ ${ctx.join("\n\n")}`
|
|
|
194
588
|
// src/core/prompts.ts
|
|
195
589
|
import { readFileSync } from "node:fs";
|
|
196
590
|
import { join } from "node:path";
|
|
591
|
+
var DEFAULT_PROMPTS_DIR = "./config/prompts";
|
|
197
592
|
var PromptLoader = class {
|
|
198
593
|
constructor(promptsDir, bot) {
|
|
199
594
|
this.promptsDir = promptsDir;
|
|
@@ -224,206 +619,8 @@ var PromptLoader = class {
|
|
|
224
619
|
}
|
|
225
620
|
};
|
|
226
621
|
|
|
227
|
-
// src/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// src/core/loaders.ts
|
|
231
|
-
import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
232
|
-
import { join as join2 } from "node:path";
|
|
233
|
-
function walk(dir) {
|
|
234
|
-
const items = [];
|
|
235
|
-
try {
|
|
236
|
-
for (const e of readdirSync(dir)) {
|
|
237
|
-
const p = join2(dir, e);
|
|
238
|
-
const st = statSync(p);
|
|
239
|
-
if (st.isDirectory()) items.push(...walk(p));
|
|
240
|
-
else if (p.endsWith(".md")) items.push(p);
|
|
241
|
-
}
|
|
242
|
-
} catch {
|
|
243
|
-
}
|
|
244
|
-
return items;
|
|
245
|
-
}
|
|
246
|
-
function loadKnowledge(root = "config/knowledge") {
|
|
247
|
-
const docs = [];
|
|
248
|
-
for (const bucket of ["base", "public", "private"]) {
|
|
249
|
-
const dir = join2(root, bucket);
|
|
250
|
-
for (const f of walk(dir)) {
|
|
251
|
-
docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
return docs;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// src/core/retrieval.ts
|
|
258
|
-
var EMB_MODEL = "text-embedding-3-large";
|
|
259
|
-
function chunk(text, max = 900) {
|
|
260
|
-
const out = [];
|
|
261
|
-
let buf = "";
|
|
262
|
-
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
263
|
-
if (`${buf}
|
|
264
|
-
${line}`.length > max) {
|
|
265
|
-
out.push(buf.trim());
|
|
266
|
-
buf = line;
|
|
267
|
-
} else buf += `
|
|
268
|
-
${line}`;
|
|
269
|
-
}
|
|
270
|
-
if (buf.trim()) out.push(buf.trim());
|
|
271
|
-
return out;
|
|
272
|
-
}
|
|
273
|
-
async function sha256(input) {
|
|
274
|
-
const data = new TextEncoder().encode(input);
|
|
275
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
276
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
277
|
-
}
|
|
278
|
-
var VectorStore = class _VectorStore {
|
|
279
|
-
constructor(client, options) {
|
|
280
|
-
this.client = client;
|
|
281
|
-
this.db = options.databaseClient ? options.databaseClient : createClient({
|
|
282
|
-
url: options.databaseUrl,
|
|
283
|
-
authToken: options.databaseAuthToken
|
|
284
|
-
});
|
|
285
|
-
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
286
|
-
this.logger = options.logger ?? createConsoleLogger();
|
|
287
|
-
}
|
|
288
|
-
client;
|
|
289
|
-
/**
|
|
290
|
-
* The libsql client backing this store. When a client was injected this is
|
|
291
|
-
* that same instance, so callers holding it (e.g. `ServerDependencies.db`)
|
|
292
|
-
* and the store share one connection.
|
|
293
|
-
*/
|
|
294
|
-
db;
|
|
295
|
-
knowledgeDir;
|
|
296
|
-
logger;
|
|
297
|
-
// On boot: ingest new chunks and embed only missing ones.
|
|
298
|
-
async build() {
|
|
299
|
-
this.logger.info("\u{1F504} Building knowledge base...");
|
|
300
|
-
await this.db.execute(`
|
|
301
|
-
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
302
|
-
`);
|
|
303
|
-
await this.db.execute(`
|
|
304
|
-
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
305
|
-
`);
|
|
306
|
-
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
307
|
-
const docs = loadKnowledge(this.knowledgeDir);
|
|
308
|
-
this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
309
|
-
const rows = [];
|
|
310
|
-
for (const d of docs) {
|
|
311
|
-
for (const part of chunk(d.text)) {
|
|
312
|
-
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
313
|
-
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
317
|
-
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
318
|
-
const UPSERT_BATCH = 500;
|
|
319
|
-
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
320
|
-
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
321
|
-
await this.db.batch(
|
|
322
|
-
batch.map((r) => ({
|
|
323
|
-
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
324
|
-
ON CONFLICT(id) DO NOTHING`,
|
|
325
|
-
args: [r.id, r.bucket, r.source, r.text]
|
|
326
|
-
})),
|
|
327
|
-
"write"
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
const ids = rows.map((r) => r.id);
|
|
331
|
-
const missing = [];
|
|
332
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
333
|
-
const batch = ids.slice(i, i + 500);
|
|
334
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
335
|
-
const res = await this.db.execute({
|
|
336
|
-
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
337
|
-
EXCEPT SELECT id FROM embeddings`,
|
|
338
|
-
args: batch
|
|
339
|
-
});
|
|
340
|
-
for (const row of res.rows) missing.push(String(row.id));
|
|
341
|
-
}
|
|
342
|
-
if (missing.length === 0) {
|
|
343
|
-
this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
347
|
-
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
348
|
-
const BATCH = 96;
|
|
349
|
-
for (let i = 0; i < missing.length; i += BATCH) {
|
|
350
|
-
const batchIds = missing.slice(i, i + BATCH);
|
|
351
|
-
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
352
|
-
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
353
|
-
await this.db.batch(
|
|
354
|
-
emb.data.map((d, idx) => ({
|
|
355
|
-
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
356
|
-
args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
|
|
357
|
-
})),
|
|
358
|
-
"write"
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
362
|
-
}
|
|
363
|
-
// Remove chunks from database that no longer exist in markdown files
|
|
364
|
-
async cleanupStaleChunks(currentIds) {
|
|
365
|
-
const result = await this.db.execute("SELECT id FROM chunks");
|
|
366
|
-
const dbIds = result.rows.map((row) => String(row.id));
|
|
367
|
-
const currentIdSet = new Set(currentIds);
|
|
368
|
-
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
369
|
-
if (staleIds.length === 0) {
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
373
|
-
const BATCH_SIZE = 500;
|
|
374
|
-
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
375
|
-
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
376
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
377
|
-
await this.db.execute({
|
|
378
|
-
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
379
|
-
args: batch
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
383
|
-
}
|
|
384
|
-
static cosine(a, b) {
|
|
385
|
-
let dot = 0;
|
|
386
|
-
let na = 0;
|
|
387
|
-
let nb = 0;
|
|
388
|
-
for (let i = 0; i < a.length; i++) {
|
|
389
|
-
dot += a[i] * b[i];
|
|
390
|
-
na += a[i] * a[i];
|
|
391
|
-
nb += b[i] * b[i];
|
|
392
|
-
}
|
|
393
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
397
|
-
* & portable).
|
|
398
|
-
*
|
|
399
|
-
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
400
|
-
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
401
|
-
* buckets can query them (`build` only writes and prunes the three the
|
|
402
|
-
* knowledge loader walks). Names are bound as query parameters, never
|
|
403
|
-
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
404
|
-
* the embedding call.
|
|
405
|
-
*/
|
|
406
|
-
async query(q, k = 6, allowed = ["base"]) {
|
|
407
|
-
if (allowed.length === 0) return [];
|
|
408
|
-
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
409
|
-
const placeholders = allowed.map(() => "?").join(",");
|
|
410
|
-
const res = await this.db.execute({
|
|
411
|
-
sql: `SELECT c.id, c.text, e.embedding
|
|
412
|
-
FROM chunks c
|
|
413
|
-
JOIN embeddings e ON e.id = c.id
|
|
414
|
-
WHERE c.bucket IN (${placeholders})`,
|
|
415
|
-
args: allowed
|
|
416
|
-
});
|
|
417
|
-
const scored = [];
|
|
418
|
-
for (const row of res.rows) {
|
|
419
|
-
const emb = JSON.parse(String(row.embedding));
|
|
420
|
-
const s = _VectorStore.cosine(qv, emb);
|
|
421
|
-
scored.push({ s, text: String(row.text) });
|
|
422
|
-
}
|
|
423
|
-
scored.sort((a, b) => b.s - a.s);
|
|
424
|
-
return scored.slice(0, k).map((r) => r.text);
|
|
425
|
-
}
|
|
426
|
-
};
|
|
622
|
+
// src/mcp-server.ts
|
|
623
|
+
init_retrieval();
|
|
427
624
|
|
|
428
625
|
// src/mcp-server/conversation-id.ts
|
|
429
626
|
function generateConversationId() {
|
|
@@ -450,28 +647,38 @@ function calculateCost(usage, rates) {
|
|
|
450
647
|
}
|
|
451
648
|
|
|
452
649
|
// src/mcp-server/logger.ts
|
|
650
|
+
init_logger();
|
|
453
651
|
var MCPLogger = class {
|
|
454
652
|
enableConsole;
|
|
455
653
|
callback;
|
|
456
654
|
logger;
|
|
457
|
-
|
|
655
|
+
logContent;
|
|
656
|
+
constructor(enableConsole = true, callback, logger, logContent = false) {
|
|
458
657
|
this.enableConsole = enableConsole;
|
|
459
658
|
this.callback = callback;
|
|
460
659
|
this.logger = logger ?? createConsoleLogger();
|
|
660
|
+
this.logContent = logContent;
|
|
461
661
|
}
|
|
462
662
|
/**
|
|
463
663
|
* Log a chat interaction
|
|
464
664
|
*/
|
|
465
665
|
async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
|
|
466
666
|
const lastUserMsg = lastUserMessage(conversationMessages);
|
|
667
|
+
const content = {
|
|
668
|
+
userMessage: scrubOutput(lastUserMsg?.content || ""),
|
|
669
|
+
conversationHistory: conversationMessages.map((msg) => ({
|
|
670
|
+
...msg,
|
|
671
|
+
content: scrubOutput(msg.content)
|
|
672
|
+
})),
|
|
673
|
+
ragContext: ragContext.map(scrubOutput),
|
|
674
|
+
response: scrubOutput(response)
|
|
675
|
+
};
|
|
676
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
467
677
|
const logEvent = {
|
|
468
|
-
timestamp
|
|
678
|
+
timestamp,
|
|
469
679
|
toolName,
|
|
470
680
|
conversationId,
|
|
471
|
-
|
|
472
|
-
conversationHistory: conversationMessages,
|
|
473
|
-
ragContext,
|
|
474
|
-
response,
|
|
681
|
+
...content,
|
|
475
682
|
duration,
|
|
476
683
|
cost
|
|
477
684
|
};
|
|
@@ -479,9 +686,27 @@ var MCPLogger = class {
|
|
|
479
686
|
this.logger.info(
|
|
480
687
|
JSON.stringify({
|
|
481
688
|
event: "mcp_chat",
|
|
482
|
-
|
|
689
|
+
timestamp,
|
|
690
|
+
toolName,
|
|
691
|
+
conversationId,
|
|
692
|
+
messageCount: conversationMessages.length,
|
|
693
|
+
ragContextCount: ragContext.length,
|
|
694
|
+
responseChars: response.length,
|
|
695
|
+
duration,
|
|
696
|
+
cost
|
|
483
697
|
})
|
|
484
698
|
);
|
|
699
|
+
if (this.logContent) {
|
|
700
|
+
this.logger.debug(
|
|
701
|
+
JSON.stringify({
|
|
702
|
+
event: "mcp_chat_content",
|
|
703
|
+
timestamp,
|
|
704
|
+
toolName,
|
|
705
|
+
conversationId,
|
|
706
|
+
...content
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
}
|
|
485
710
|
}
|
|
486
711
|
if (this.callback) {
|
|
487
712
|
try {
|
|
@@ -492,11 +717,12 @@ var MCPLogger = class {
|
|
|
492
717
|
}
|
|
493
718
|
}
|
|
494
719
|
};
|
|
495
|
-
function createLogger(enableConsole = true, callback, logger) {
|
|
496
|
-
return new MCPLogger(enableConsole, callback, logger);
|
|
720
|
+
function createLogger(enableConsole = true, callback, logger, logContent = false) {
|
|
721
|
+
return new MCPLogger(enableConsole, callback, logger, logContent);
|
|
497
722
|
}
|
|
498
723
|
|
|
499
724
|
// src/mcp-server/rate-limiter.ts
|
|
725
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
500
726
|
var RateLimiter = class {
|
|
501
727
|
requestMap;
|
|
502
728
|
limit;
|
|
@@ -505,9 +731,9 @@ var RateLimiter = class {
|
|
|
505
731
|
* Create a new rate limiter
|
|
506
732
|
*
|
|
507
733
|
* @param limit - Maximum requests allowed in the time window
|
|
508
|
-
* @param windowMs - Time window in milliseconds (default:
|
|
734
|
+
* @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
|
|
509
735
|
*/
|
|
510
|
-
constructor(limit, windowMs =
|
|
736
|
+
constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
511
737
|
this.requestMap = /* @__PURE__ */ new Map();
|
|
512
738
|
this.limit = limit;
|
|
513
739
|
this.windowMs = windowMs;
|
|
@@ -571,7 +797,7 @@ var RateLimiter = class {
|
|
|
571
797
|
return Math.max(0, timeUntilReset);
|
|
572
798
|
}
|
|
573
799
|
};
|
|
574
|
-
function
|
|
800
|
+
function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
575
801
|
return limit ? new RateLimiter(limit, windowMs) : null;
|
|
576
802
|
}
|
|
577
803
|
|
|
@@ -621,11 +847,15 @@ async function createMCPServer(config) {
|
|
|
621
847
|
const log = resolveLogger(config.logger, config.logLevel);
|
|
622
848
|
log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
|
|
623
849
|
const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
|
|
624
|
-
const
|
|
625
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
850
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
626
851
|
const transportMode = config.transport || "stdio";
|
|
627
|
-
const rateLimiter =
|
|
628
|
-
const logger = createLogger(
|
|
852
|
+
const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
|
|
853
|
+
const logger = createLogger(
|
|
854
|
+
config.logging?.console !== false,
|
|
855
|
+
config.logging?.onChat,
|
|
856
|
+
log,
|
|
857
|
+
config.logging?.content === true
|
|
858
|
+
);
|
|
629
859
|
const publicToolConfig = {
|
|
630
860
|
enabled: config.tools?.public?.enabled !== false,
|
|
631
861
|
name: config.tools?.public?.name || "chat_public",
|
|
@@ -640,13 +870,22 @@ async function createMCPServer(config) {
|
|
|
640
870
|
apiKey: config.openai.apiKey
|
|
641
871
|
});
|
|
642
872
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
873
|
+
let store;
|
|
874
|
+
if (config.retriever) {
|
|
875
|
+
store = config.retriever;
|
|
876
|
+
} else {
|
|
877
|
+
if (!config.database) {
|
|
878
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
879
|
+
}
|
|
880
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
881
|
+
const db = await openLibsqlClient2(config.database);
|
|
882
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
883
|
+
databaseClient: db,
|
|
884
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
885
|
+
logger: log
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
await store.build?.();
|
|
650
889
|
log.info("\u2705 Knowledge base ready");
|
|
651
890
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
652
891
|
const server = new McpServer({
|
|
@@ -695,6 +934,7 @@ async function createMCPServer(config) {
|
|
|
695
934
|
buckets,
|
|
696
935
|
rewriteQuery: config.rewriteQuery,
|
|
697
936
|
rerankContext: config.rerankContext,
|
|
937
|
+
fallbackFn: config.fallbackFn,
|
|
698
938
|
logger: log
|
|
699
939
|
});
|
|
700
940
|
const result = await answerOnce({
|
|
@@ -784,6 +1024,7 @@ async function createMCPServer(config) {
|
|
|
784
1024
|
buckets,
|
|
785
1025
|
rewriteQuery: config.rewriteQuery,
|
|
786
1026
|
rerankContext: config.rerankContext,
|
|
1027
|
+
fallbackFn: config.fallbackFn,
|
|
787
1028
|
logger: log
|
|
788
1029
|
});
|
|
789
1030
|
const result = await answerOnce({
|