@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/index.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,16 +35,402 @@ 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/buildLock.ts
|
|
39
|
+
function canAcquireBuildLock(existing, holderId, now, staleMs = BUILD_LOCK_STALE_MS) {
|
|
40
|
+
if (!existing) return true;
|
|
41
|
+
if (existing.holderId === holderId) return true;
|
|
42
|
+
return now - existing.heartbeatAt >= staleMs;
|
|
43
|
+
}
|
|
44
|
+
function ensureBuildLockSchema(client) {
|
|
45
|
+
let ready = schemaReady.get(client);
|
|
46
|
+
if (!ready) {
|
|
47
|
+
ready = client.execute(
|
|
48
|
+
`CREATE TABLE IF NOT EXISTS chatter_build_lock (
|
|
49
|
+
lock_key TEXT PRIMARY KEY,
|
|
50
|
+
holder_id TEXT NOT NULL,
|
|
51
|
+
heartbeat_at INTEGER NOT NULL
|
|
52
|
+
)`
|
|
53
|
+
).catch((error) => {
|
|
54
|
+
schemaReady.delete(client);
|
|
55
|
+
throw error;
|
|
56
|
+
});
|
|
57
|
+
schemaReady.set(client, ready);
|
|
58
|
+
}
|
|
59
|
+
return ready;
|
|
60
|
+
}
|
|
61
|
+
function createTursoBuildLock(client) {
|
|
62
|
+
return {
|
|
63
|
+
async tryAcquire(key, holderId, now, staleMs) {
|
|
64
|
+
await ensureBuildLockSchema(client);
|
|
65
|
+
const result = await client.execute({
|
|
66
|
+
sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
|
|
67
|
+
ON CONFLICT(lock_key) DO UPDATE SET
|
|
68
|
+
holder_id = excluded.holder_id,
|
|
69
|
+
heartbeat_at = excluded.heartbeat_at
|
|
70
|
+
WHERE chatter_build_lock.holder_id = excluded.holder_id
|
|
71
|
+
OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
|
|
72
|
+
RETURNING holder_id`,
|
|
73
|
+
args: [key, holderId, now, staleMs]
|
|
74
|
+
});
|
|
75
|
+
const row = result.rows[0];
|
|
76
|
+
return Boolean(row) && row.holder_id === holderId;
|
|
77
|
+
},
|
|
78
|
+
async release(key, holderId) {
|
|
79
|
+
await ensureBuildLockSchema(client);
|
|
80
|
+
await client.execute({
|
|
81
|
+
sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
|
|
82
|
+
args: [key, holderId]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
|
|
88
|
+
var init_buildLock = __esm({
|
|
89
|
+
"src/core/buildLock.ts"() {
|
|
90
|
+
"use strict";
|
|
91
|
+
BUILD_LOCK_KEY = "knowledge_build";
|
|
92
|
+
BUILD_LOCK_STALE_MS = 10 * 6e4;
|
|
93
|
+
schemaReady = /* @__PURE__ */ new WeakMap();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/core/loaders.ts
|
|
98
|
+
function walk(dir) {
|
|
99
|
+
const items = [];
|
|
100
|
+
try {
|
|
101
|
+
for (const e of (0, import_node_fs.readdirSync)(dir)) {
|
|
102
|
+
const p = (0, import_node_path.join)(dir, e);
|
|
103
|
+
const st = (0, import_node_fs.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 = (0, import_node_path.join)(root, bucket);
|
|
115
|
+
for (const f of walk(dir)) {
|
|
116
|
+
docs.push({ id: f, text: (0, import_node_fs.readFileSync)(f, "utf8"), source: f, bucket });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return docs;
|
|
120
|
+
}
|
|
121
|
+
var import_node_fs, import_node_path;
|
|
122
|
+
var init_loaders = __esm({
|
|
123
|
+
"src/core/loaders.ts"() {
|
|
124
|
+
"use strict";
|
|
125
|
+
import_node_fs = require("node:fs");
|
|
126
|
+
import_node_path = require("node:path");
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// src/core/logger.ts
|
|
131
|
+
function createConsoleLogger(level = "info") {
|
|
132
|
+
const threshold = LEVEL_ORDER[level];
|
|
133
|
+
const write = (at) => (...args) => {
|
|
134
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
135
|
+
console.error(`[${at}]`, ...args);
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
debug: write("debug"),
|
|
139
|
+
info: write("info"),
|
|
140
|
+
warn: write("warn"),
|
|
141
|
+
error: write("error")
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function resolveLogger(logger, level) {
|
|
145
|
+
return logger ?? createConsoleLogger(level);
|
|
146
|
+
}
|
|
147
|
+
var LEVEL_ORDER;
|
|
148
|
+
var init_logger = __esm({
|
|
149
|
+
"src/core/logger.ts"() {
|
|
150
|
+
"use strict";
|
|
151
|
+
LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// src/core/retrieval.ts
|
|
156
|
+
var retrieval_exports = {};
|
|
157
|
+
__export(retrieval_exports, {
|
|
158
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
|
|
159
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
160
|
+
VectorStore: () => VectorStore,
|
|
161
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
162
|
+
openLibsqlClient: () => openLibsqlClient,
|
|
163
|
+
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
164
|
+
});
|
|
165
|
+
function createOpenAIEmbedder(client) {
|
|
166
|
+
return async (input) => {
|
|
167
|
+
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
168
|
+
return res.data.map((d) => d.embedding);
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function wrapMissingLibsqlError(cause) {
|
|
172
|
+
return new Error(
|
|
173
|
+
"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.",
|
|
174
|
+
{ cause }
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
async function openLibsqlClient(database) {
|
|
178
|
+
const mod = await import("@libsql/client").catch((error) => {
|
|
179
|
+
throw wrapMissingLibsqlError(error);
|
|
180
|
+
});
|
|
181
|
+
return mod.createClient({ url: database.url, authToken: database.authToken });
|
|
182
|
+
}
|
|
183
|
+
function chunk(text, max = 900) {
|
|
184
|
+
const out = [];
|
|
185
|
+
let buf = "";
|
|
186
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
187
|
+
if (`${buf}
|
|
188
|
+
${line}`.length > max) {
|
|
189
|
+
out.push(buf.trim());
|
|
190
|
+
buf = line;
|
|
191
|
+
} else buf += `
|
|
192
|
+
${line}`;
|
|
193
|
+
}
|
|
194
|
+
if (buf.trim()) out.push(buf.trim());
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
async function sha256(input) {
|
|
198
|
+
const data = new TextEncoder().encode(input);
|
|
199
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
200
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
201
|
+
}
|
|
202
|
+
var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
|
|
203
|
+
var init_retrieval = __esm({
|
|
204
|
+
"src/core/retrieval.ts"() {
|
|
205
|
+
"use strict";
|
|
206
|
+
init_buildLock();
|
|
207
|
+
init_loaders();
|
|
208
|
+
init_logger();
|
|
209
|
+
EMB_MODEL = "text-embedding-3-large";
|
|
210
|
+
DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
|
|
211
|
+
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.";
|
|
212
|
+
VectorStore = class _VectorStore {
|
|
213
|
+
constructor(embed, options) {
|
|
214
|
+
this.embed = embed;
|
|
215
|
+
this.db = options.databaseClient;
|
|
216
|
+
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
217
|
+
this.logger = options.logger ?? createConsoleLogger();
|
|
218
|
+
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
219
|
+
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
220
|
+
this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
|
|
221
|
+
}
|
|
222
|
+
embed;
|
|
223
|
+
/**
|
|
224
|
+
* The libsql client backing this store - the same instance passed in as
|
|
225
|
+
* `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
|
|
226
|
+
* and the store share one connection.
|
|
227
|
+
*/
|
|
228
|
+
db;
|
|
229
|
+
knowledgeDir;
|
|
230
|
+
logger;
|
|
231
|
+
buildLock;
|
|
232
|
+
instanceId;
|
|
233
|
+
buildLockStaleMs;
|
|
234
|
+
/**
|
|
235
|
+
* On boot: ingest new chunks and embed only missing ones.
|
|
236
|
+
*
|
|
237
|
+
* The whole ingest runs under a single-writer lock held in the database
|
|
238
|
+
* (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
|
|
239
|
+
* every chunk id the current `knowledgeDir` did not produce. Without the
|
|
240
|
+
* lock, a second instance booting mid-build diffs against a database the
|
|
241
|
+
* first one is still writing and deletes its chunks. An instance that
|
|
242
|
+
* cannot take the lock skips its build entirely and serves what the holder
|
|
243
|
+
* has already written, rather than racing it.
|
|
244
|
+
*/
|
|
245
|
+
async build() {
|
|
246
|
+
this.logger.info("Building knowledge base...");
|
|
247
|
+
await this.db.execute(`
|
|
248
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
249
|
+
`);
|
|
250
|
+
await this.db.execute(`
|
|
251
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
252
|
+
`);
|
|
253
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
254
|
+
if (!await this.renewBuildLock()) {
|
|
255
|
+
this.logger.warn(
|
|
256
|
+
"Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
|
|
257
|
+
);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
try {
|
|
261
|
+
await this.buildUnderLock();
|
|
262
|
+
} finally {
|
|
263
|
+
await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
|
|
267
|
+
renewBuildLock() {
|
|
268
|
+
return this.buildLock.tryAcquire(
|
|
269
|
+
BUILD_LOCK_KEY,
|
|
270
|
+
this.instanceId,
|
|
271
|
+
Date.now(),
|
|
272
|
+
this.buildLockStaleMs
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
/** The ingest itself. Only ever runs with the build lock held. */
|
|
276
|
+
async buildUnderLock() {
|
|
277
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
278
|
+
this.logger.info(`Loaded ${docs.length} knowledge documents`);
|
|
279
|
+
if (docs.length === 0) {
|
|
280
|
+
const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
|
|
281
|
+
const existingCount = Number(existing.rows[0]?.count ?? 0);
|
|
282
|
+
if (existingCount > 0) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`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.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const rows = [];
|
|
291
|
+
for (const d of docs) {
|
|
292
|
+
for (const part of chunk(d.text)) {
|
|
293
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
294
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
298
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
299
|
+
const UPSERT_BATCH = 500;
|
|
300
|
+
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
301
|
+
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
302
|
+
await this.db.batch(
|
|
303
|
+
batch.map((r) => ({
|
|
304
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
305
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
306
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
307
|
+
})),
|
|
308
|
+
"write"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
const ids = rows.map((r) => r.id);
|
|
312
|
+
const missing = [];
|
|
313
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
314
|
+
const batch = ids.slice(i, i + 500);
|
|
315
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
316
|
+
const res = await this.db.execute({
|
|
317
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
318
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
319
|
+
args: batch
|
|
320
|
+
});
|
|
321
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
322
|
+
}
|
|
323
|
+
if (missing.length === 0) {
|
|
324
|
+
this.logger.info("No new chunks to embed - knowledge base is up to date");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
328
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
329
|
+
const BATCH = 96;
|
|
330
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
331
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
332
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
333
|
+
const vectors = await this.embed(inputs);
|
|
334
|
+
await this.db.batch(
|
|
335
|
+
vectors.map((embedding, idx) => ({
|
|
336
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
337
|
+
args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
|
|
338
|
+
})),
|
|
339
|
+
"write"
|
|
340
|
+
);
|
|
341
|
+
if (!await this.renewBuildLock()) {
|
|
342
|
+
this.logger.warn(
|
|
343
|
+
"Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
348
|
+
}
|
|
349
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
350
|
+
async cleanupStaleChunks(currentIds) {
|
|
351
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
352
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
353
|
+
const currentIdSet = new Set(currentIds);
|
|
354
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
355
|
+
if (staleIds.length === 0) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
|
|
359
|
+
const BATCH_SIZE = 500;
|
|
360
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
361
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
362
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
363
|
+
await this.db.batch(
|
|
364
|
+
[
|
|
365
|
+
{ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
|
|
366
|
+
{ sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
|
|
367
|
+
],
|
|
368
|
+
"write"
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
372
|
+
}
|
|
373
|
+
static cosine(a, b) {
|
|
374
|
+
let dot = 0;
|
|
375
|
+
let na = 0;
|
|
376
|
+
let nb = 0;
|
|
377
|
+
for (let i = 0; i < a.length; i++) {
|
|
378
|
+
dot += a[i] * b[i];
|
|
379
|
+
na += a[i] * a[i];
|
|
380
|
+
nb += b[i] * b[i];
|
|
381
|
+
}
|
|
382
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
386
|
+
* & portable).
|
|
387
|
+
*
|
|
388
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
389
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
390
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
391
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
392
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
393
|
+
* the embedding call.
|
|
394
|
+
*/
|
|
395
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
396
|
+
if (allowed.length === 0) return [];
|
|
397
|
+
const [qv] = await this.embed([q]);
|
|
398
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
399
|
+
const res = await this.db.execute({
|
|
400
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
401
|
+
FROM chunks c
|
|
402
|
+
JOIN embeddings e ON e.id = c.id
|
|
403
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
404
|
+
args: allowed
|
|
405
|
+
});
|
|
406
|
+
const scored = [];
|
|
407
|
+
for (const row of res.rows) {
|
|
408
|
+
const emb = JSON.parse(String(row.embedding));
|
|
409
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
410
|
+
scored.push({ s, text: String(row.text) });
|
|
411
|
+
}
|
|
412
|
+
scored.sort((a, b) => b.s - a.s);
|
|
413
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
30
419
|
// src/index.ts
|
|
31
420
|
var index_exports = {};
|
|
32
421
|
__export(index_exports, {
|
|
33
422
|
ApiKeyManager: () => ApiKeyManager,
|
|
423
|
+
BUILD_LOCK_KEY: () => BUILD_LOCK_KEY,
|
|
424
|
+
BUILD_LOCK_STALE_MS: () => BUILD_LOCK_STALE_MS,
|
|
425
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
34
426
|
DEFAULT_MODEL: () => DEFAULT_MODEL,
|
|
427
|
+
DEFAULT_PROMPTS_DIR: () => DEFAULT_PROMPTS_DIR,
|
|
35
428
|
PromptLoader: () => PromptLoader,
|
|
36
429
|
VectorStore: () => VectorStore,
|
|
37
430
|
answerOnce: () => answerOnce,
|
|
38
431
|
answerStream: () => answerStream,
|
|
39
432
|
applyTransformReply: () => applyTransformReply,
|
|
433
|
+
canAcquireBuildLock: () => canAcquireBuildLock,
|
|
40
434
|
chatBodyLimit: () => chatBodyLimit,
|
|
41
435
|
completeOnce: () => completeOnce,
|
|
42
436
|
completeStream: () => completeStream,
|
|
@@ -45,12 +439,14 @@ __export(index_exports, {
|
|
|
45
439
|
createConsoleLogger: () => createConsoleLogger,
|
|
46
440
|
createJWTMiddleware: () => createJWTMiddleware,
|
|
47
441
|
createMCPServer: () => createMCPServer,
|
|
48
|
-
|
|
442
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
443
|
+
createRateLimiter: () => createRateLimiter,
|
|
49
444
|
createReferrerCheck: () => createReferrerCheck,
|
|
50
445
|
createServer: () => createServer,
|
|
51
446
|
createSession: () => createSession,
|
|
52
447
|
createSessionMiddleware: () => createSessionMiddleware,
|
|
53
448
|
createShortener: () => createShortener,
|
|
449
|
+
createTursoBuildLock: () => createTursoBuildLock,
|
|
54
450
|
defaultBuckets: () => defaultBuckets,
|
|
55
451
|
demoRoutes: () => demoRoutes,
|
|
56
452
|
detectLeakage: () => detectLeakage,
|
|
@@ -286,7 +682,8 @@ async function answerOnce({
|
|
|
286
682
|
async function applyTransformReply(transformReply, input, logger) {
|
|
287
683
|
if (!transformReply) return input.text;
|
|
288
684
|
try {
|
|
289
|
-
|
|
685
|
+
const result = await transformReply(input);
|
|
686
|
+
return typeof result === "string" ? result : null;
|
|
290
687
|
} catch (error) {
|
|
291
688
|
logger?.error(
|
|
292
689
|
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
@@ -351,51 +748,10 @@ async function resolveBuckets({
|
|
|
351
748
|
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
352
749
|
}
|
|
353
750
|
|
|
354
|
-
// src/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const items = [];
|
|
359
|
-
try {
|
|
360
|
-
for (const e of (0, import_node_fs.readdirSync)(dir)) {
|
|
361
|
-
const p = (0, import_node_path.join)(dir, e);
|
|
362
|
-
const st = (0, import_node_fs.statSync)(p);
|
|
363
|
-
if (st.isDirectory()) items.push(...walk(p));
|
|
364
|
-
else if (p.endsWith(".md")) items.push(p);
|
|
365
|
-
}
|
|
366
|
-
} catch {
|
|
367
|
-
}
|
|
368
|
-
return items;
|
|
369
|
-
}
|
|
370
|
-
function loadKnowledge(root = "config/knowledge") {
|
|
371
|
-
const docs = [];
|
|
372
|
-
for (const bucket of ["base", "public", "private"]) {
|
|
373
|
-
const dir = (0, import_node_path.join)(root, bucket);
|
|
374
|
-
for (const f of walk(dir)) {
|
|
375
|
-
docs.push({ id: f, text: (0, import_node_fs.readFileSync)(f, "utf8"), source: f, bucket });
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
return docs;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// src/core/logger.ts
|
|
382
|
-
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
383
|
-
function createConsoleLogger(level = "info") {
|
|
384
|
-
const threshold = LEVEL_ORDER[level];
|
|
385
|
-
const write = (at) => (...args) => {
|
|
386
|
-
if (LEVEL_ORDER[at] < threshold) return;
|
|
387
|
-
console.error(`[${at}]`, ...args);
|
|
388
|
-
};
|
|
389
|
-
return {
|
|
390
|
-
debug: write("debug"),
|
|
391
|
-
info: write("info"),
|
|
392
|
-
warn: write("warn"),
|
|
393
|
-
error: write("error")
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
function resolveLogger(logger, level) {
|
|
397
|
-
return logger ?? createConsoleLogger(level);
|
|
398
|
-
}
|
|
751
|
+
// src/index.ts
|
|
752
|
+
init_buildLock();
|
|
753
|
+
init_loaders();
|
|
754
|
+
init_logger();
|
|
399
755
|
|
|
400
756
|
// src/core/messages.ts
|
|
401
757
|
function isClientRole(role) {
|
|
@@ -462,6 +818,7 @@ async function prepareChat({
|
|
|
462
818
|
sender,
|
|
463
819
|
rewriteQuery,
|
|
464
820
|
rerankContext,
|
|
821
|
+
fallbackFn,
|
|
465
822
|
logger
|
|
466
823
|
}) {
|
|
467
824
|
const lastUserMsg = lastUserMessage(messages);
|
|
@@ -478,21 +835,39 @@ async function prepareChat({
|
|
|
478
835
|
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
479
836
|
}
|
|
480
837
|
}
|
|
481
|
-
|
|
838
|
+
const resolvedBuckets = buckets ?? defaultBuckets(mode);
|
|
839
|
+
const retrievedChunks = await store.query(query, topK, resolvedBuckets);
|
|
840
|
+
let ctx = retrievedChunks;
|
|
482
841
|
if (rerankContext) {
|
|
483
842
|
try {
|
|
484
|
-
const reranked = await rerankContext({ query, chunks: ctx });
|
|
843
|
+
const reranked = await rerankContext({ query, chunks: [...ctx] });
|
|
485
844
|
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
486
845
|
} catch (error) {
|
|
487
846
|
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
488
847
|
}
|
|
489
848
|
}
|
|
849
|
+
let fallback;
|
|
850
|
+
if (ctx.length === 0 && fallbackFn) {
|
|
851
|
+
try {
|
|
852
|
+
const guidance = await fallbackFn({
|
|
853
|
+
query,
|
|
854
|
+
mode,
|
|
855
|
+
buckets: resolvedBuckets,
|
|
856
|
+
sender,
|
|
857
|
+
retrievedChunks
|
|
858
|
+
});
|
|
859
|
+
if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
|
|
860
|
+
} catch (error) {
|
|
861
|
+
logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
490
864
|
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
491
865
|
const hint = channelHint?.trim();
|
|
492
866
|
const system = [
|
|
493
867
|
prompts.baseSystemRules,
|
|
494
868
|
persona,
|
|
495
869
|
...hint ? [hint] : [],
|
|
870
|
+
...fallback ? [fallback] : [],
|
|
496
871
|
`${label}:
|
|
497
872
|
${ctx.join("\n\n")}`
|
|
498
873
|
].join("\n\n");
|
|
@@ -502,6 +877,7 @@ ${ctx.join("\n\n")}`
|
|
|
502
877
|
// src/core/prompts.ts
|
|
503
878
|
var import_node_fs2 = require("node:fs");
|
|
504
879
|
var import_node_path2 = require("node:path");
|
|
880
|
+
var DEFAULT_PROMPTS_DIR = "./config/prompts";
|
|
505
881
|
var PromptLoader = class {
|
|
506
882
|
constructor(promptsDir, bot) {
|
|
507
883
|
this.promptsDir = promptsDir;
|
|
@@ -532,196 +908,32 @@ var PromptLoader = class {
|
|
|
532
908
|
}
|
|
533
909
|
};
|
|
534
910
|
|
|
535
|
-
// src/
|
|
536
|
-
|
|
537
|
-
var EMB_MODEL = "text-embedding-3-large";
|
|
538
|
-
function chunk(text, max = 900) {
|
|
539
|
-
const out = [];
|
|
540
|
-
let buf = "";
|
|
541
|
-
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
542
|
-
if (`${buf}
|
|
543
|
-
${line}`.length > max) {
|
|
544
|
-
out.push(buf.trim());
|
|
545
|
-
buf = line;
|
|
546
|
-
} else buf += `
|
|
547
|
-
${line}`;
|
|
548
|
-
}
|
|
549
|
-
if (buf.trim()) out.push(buf.trim());
|
|
550
|
-
return out;
|
|
551
|
-
}
|
|
552
|
-
async function sha256(input) {
|
|
553
|
-
const data = new TextEncoder().encode(input);
|
|
554
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
555
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
556
|
-
}
|
|
557
|
-
var VectorStore = class _VectorStore {
|
|
558
|
-
constructor(client, options) {
|
|
559
|
-
this.client = client;
|
|
560
|
-
this.db = options.databaseClient ? options.databaseClient : (0, import_client.createClient)({
|
|
561
|
-
url: options.databaseUrl,
|
|
562
|
-
authToken: options.databaseAuthToken
|
|
563
|
-
});
|
|
564
|
-
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
565
|
-
this.logger = options.logger ?? createConsoleLogger();
|
|
566
|
-
}
|
|
567
|
-
client;
|
|
568
|
-
/**
|
|
569
|
-
* The libsql client backing this store. When a client was injected this is
|
|
570
|
-
* that same instance, so callers holding it (e.g. `ServerDependencies.db`)
|
|
571
|
-
* and the store share one connection.
|
|
572
|
-
*/
|
|
573
|
-
db;
|
|
574
|
-
knowledgeDir;
|
|
575
|
-
logger;
|
|
576
|
-
// On boot: ingest new chunks and embed only missing ones.
|
|
577
|
-
async build() {
|
|
578
|
-
this.logger.info("\u{1F504} Building knowledge base...");
|
|
579
|
-
await this.db.execute(`
|
|
580
|
-
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
581
|
-
`);
|
|
582
|
-
await this.db.execute(`
|
|
583
|
-
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
584
|
-
`);
|
|
585
|
-
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
586
|
-
const docs = loadKnowledge(this.knowledgeDir);
|
|
587
|
-
this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
588
|
-
const rows = [];
|
|
589
|
-
for (const d of docs) {
|
|
590
|
-
for (const part of chunk(d.text)) {
|
|
591
|
-
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
592
|
-
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
596
|
-
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
597
|
-
const UPSERT_BATCH = 500;
|
|
598
|
-
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
599
|
-
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
600
|
-
await this.db.batch(
|
|
601
|
-
batch.map((r) => ({
|
|
602
|
-
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
603
|
-
ON CONFLICT(id) DO NOTHING`,
|
|
604
|
-
args: [r.id, r.bucket, r.source, r.text]
|
|
605
|
-
})),
|
|
606
|
-
"write"
|
|
607
|
-
);
|
|
608
|
-
}
|
|
609
|
-
const ids = rows.map((r) => r.id);
|
|
610
|
-
const missing = [];
|
|
611
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
612
|
-
const batch = ids.slice(i, i + 500);
|
|
613
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
614
|
-
const res = await this.db.execute({
|
|
615
|
-
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
616
|
-
EXCEPT SELECT id FROM embeddings`,
|
|
617
|
-
args: batch
|
|
618
|
-
});
|
|
619
|
-
for (const row of res.rows) missing.push(String(row.id));
|
|
620
|
-
}
|
|
621
|
-
if (missing.length === 0) {
|
|
622
|
-
this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
626
|
-
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
627
|
-
const BATCH = 96;
|
|
628
|
-
for (let i = 0; i < missing.length; i += BATCH) {
|
|
629
|
-
const batchIds = missing.slice(i, i + BATCH);
|
|
630
|
-
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
631
|
-
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
632
|
-
await this.db.batch(
|
|
633
|
-
emb.data.map((d, idx) => ({
|
|
634
|
-
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
635
|
-
args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
|
|
636
|
-
})),
|
|
637
|
-
"write"
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
641
|
-
}
|
|
642
|
-
// Remove chunks from database that no longer exist in markdown files
|
|
643
|
-
async cleanupStaleChunks(currentIds) {
|
|
644
|
-
const result = await this.db.execute("SELECT id FROM chunks");
|
|
645
|
-
const dbIds = result.rows.map((row) => String(row.id));
|
|
646
|
-
const currentIdSet = new Set(currentIds);
|
|
647
|
-
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
648
|
-
if (staleIds.length === 0) {
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
652
|
-
const BATCH_SIZE = 500;
|
|
653
|
-
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
654
|
-
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
655
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
656
|
-
await this.db.execute({
|
|
657
|
-
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
658
|
-
args: batch
|
|
659
|
-
});
|
|
660
|
-
}
|
|
661
|
-
this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
662
|
-
}
|
|
663
|
-
static cosine(a, b) {
|
|
664
|
-
let dot = 0;
|
|
665
|
-
let na = 0;
|
|
666
|
-
let nb = 0;
|
|
667
|
-
for (let i = 0; i < a.length; i++) {
|
|
668
|
-
dot += a[i] * b[i];
|
|
669
|
-
na += a[i] * a[i];
|
|
670
|
-
nb += b[i] * b[i];
|
|
671
|
-
}
|
|
672
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
673
|
-
}
|
|
674
|
-
/**
|
|
675
|
-
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
676
|
-
* & portable).
|
|
677
|
-
*
|
|
678
|
-
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
679
|
-
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
680
|
-
* buckets can query them (`build` only writes and prunes the three the
|
|
681
|
-
* knowledge loader walks). Names are bound as query parameters, never
|
|
682
|
-
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
683
|
-
* the embedding call.
|
|
684
|
-
*/
|
|
685
|
-
async query(q, k = 6, allowed = ["base"]) {
|
|
686
|
-
if (allowed.length === 0) return [];
|
|
687
|
-
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
688
|
-
const placeholders = allowed.map(() => "?").join(",");
|
|
689
|
-
const res = await this.db.execute({
|
|
690
|
-
sql: `SELECT c.id, c.text, e.embedding
|
|
691
|
-
FROM chunks c
|
|
692
|
-
JOIN embeddings e ON e.id = c.id
|
|
693
|
-
WHERE c.bucket IN (${placeholders})`,
|
|
694
|
-
args: allowed
|
|
695
|
-
});
|
|
696
|
-
const scored = [];
|
|
697
|
-
for (const row of res.rows) {
|
|
698
|
-
const emb = JSON.parse(String(row.embedding));
|
|
699
|
-
const s = _VectorStore.cosine(qv, emb);
|
|
700
|
-
scored.push({ s, text: String(row.text) });
|
|
701
|
-
}
|
|
702
|
-
scored.sort((a, b) => b.s - a.s);
|
|
703
|
-
return scored.slice(0, k).map((r) => r.text);
|
|
704
|
-
}
|
|
705
|
-
};
|
|
911
|
+
// src/index.ts
|
|
912
|
+
init_retrieval();
|
|
706
913
|
|
|
707
914
|
// src/core/session.ts
|
|
708
915
|
var import_node_crypto2 = require("node:crypto");
|
|
916
|
+
var SESSION_KEY_PREFIX = "session_";
|
|
917
|
+
var DEFAULT_SESSION_MAX_REQUESTS = 20;
|
|
918
|
+
var DEFAULT_SESSION_TTL_SECONDS = 3600;
|
|
919
|
+
var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
709
920
|
var sessions = /* @__PURE__ */ new Map();
|
|
710
|
-
var sessionSweeper = setInterval(
|
|
711
|
-
()
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
sessions.delete(key);
|
|
716
|
-
}
|
|
921
|
+
var sessionSweeper = setInterval(() => {
|
|
922
|
+
const now = Date.now();
|
|
923
|
+
for (const [key, data] of Array.from(sessions.entries())) {
|
|
924
|
+
if (now > data.expiresAt) {
|
|
925
|
+
sessions.delete(key);
|
|
717
926
|
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
);
|
|
927
|
+
}
|
|
928
|
+
}, SESSION_SWEEP_INTERVAL_MS);
|
|
721
929
|
sessionSweeper.unref?.();
|
|
722
930
|
function createSession(options = {}) {
|
|
723
|
-
const {
|
|
724
|
-
|
|
931
|
+
const {
|
|
932
|
+
maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
|
|
933
|
+
ttl = DEFAULT_SESSION_TTL_SECONDS,
|
|
934
|
+
metadata
|
|
935
|
+
} = options;
|
|
936
|
+
const key = `${SESSION_KEY_PREFIX}${(0, import_node_crypto2.randomUUID)()}`;
|
|
725
937
|
const now = Date.now();
|
|
726
938
|
const session = {
|
|
727
939
|
key,
|
|
@@ -757,6 +969,8 @@ function getActiveSessions() {
|
|
|
757
969
|
|
|
758
970
|
// src/mcp-server.ts
|
|
759
971
|
var import_openai = __toESM(require("openai"));
|
|
972
|
+
init_logger();
|
|
973
|
+
init_retrieval();
|
|
760
974
|
|
|
761
975
|
// src/mcp-server/conversation-id.ts
|
|
762
976
|
function generateConversationId() {
|
|
@@ -783,28 +997,38 @@ function calculateCost(usage, rates) {
|
|
|
783
997
|
}
|
|
784
998
|
|
|
785
999
|
// src/mcp-server/logger.ts
|
|
1000
|
+
init_logger();
|
|
786
1001
|
var MCPLogger = class {
|
|
787
1002
|
enableConsole;
|
|
788
1003
|
callback;
|
|
789
1004
|
logger;
|
|
790
|
-
|
|
1005
|
+
logContent;
|
|
1006
|
+
constructor(enableConsole = true, callback, logger, logContent = false) {
|
|
791
1007
|
this.enableConsole = enableConsole;
|
|
792
1008
|
this.callback = callback;
|
|
793
1009
|
this.logger = logger ?? createConsoleLogger();
|
|
1010
|
+
this.logContent = logContent;
|
|
794
1011
|
}
|
|
795
1012
|
/**
|
|
796
1013
|
* Log a chat interaction
|
|
797
1014
|
*/
|
|
798
1015
|
async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
|
|
799
1016
|
const lastUserMsg = lastUserMessage(conversationMessages);
|
|
1017
|
+
const content = {
|
|
1018
|
+
userMessage: scrubOutput(lastUserMsg?.content || ""),
|
|
1019
|
+
conversationHistory: conversationMessages.map((msg) => ({
|
|
1020
|
+
...msg,
|
|
1021
|
+
content: scrubOutput(msg.content)
|
|
1022
|
+
})),
|
|
1023
|
+
ragContext: ragContext.map(scrubOutput),
|
|
1024
|
+
response: scrubOutput(response)
|
|
1025
|
+
};
|
|
1026
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
800
1027
|
const logEvent = {
|
|
801
|
-
timestamp
|
|
1028
|
+
timestamp,
|
|
802
1029
|
toolName,
|
|
803
1030
|
conversationId,
|
|
804
|
-
|
|
805
|
-
conversationHistory: conversationMessages,
|
|
806
|
-
ragContext,
|
|
807
|
-
response,
|
|
1031
|
+
...content,
|
|
808
1032
|
duration,
|
|
809
1033
|
cost
|
|
810
1034
|
};
|
|
@@ -812,9 +1036,27 @@ var MCPLogger = class {
|
|
|
812
1036
|
this.logger.info(
|
|
813
1037
|
JSON.stringify({
|
|
814
1038
|
event: "mcp_chat",
|
|
815
|
-
|
|
1039
|
+
timestamp,
|
|
1040
|
+
toolName,
|
|
1041
|
+
conversationId,
|
|
1042
|
+
messageCount: conversationMessages.length,
|
|
1043
|
+
ragContextCount: ragContext.length,
|
|
1044
|
+
responseChars: response.length,
|
|
1045
|
+
duration,
|
|
1046
|
+
cost
|
|
816
1047
|
})
|
|
817
1048
|
);
|
|
1049
|
+
if (this.logContent) {
|
|
1050
|
+
this.logger.debug(
|
|
1051
|
+
JSON.stringify({
|
|
1052
|
+
event: "mcp_chat_content",
|
|
1053
|
+
timestamp,
|
|
1054
|
+
toolName,
|
|
1055
|
+
conversationId,
|
|
1056
|
+
...content
|
|
1057
|
+
})
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
818
1060
|
}
|
|
819
1061
|
if (this.callback) {
|
|
820
1062
|
try {
|
|
@@ -825,11 +1067,12 @@ var MCPLogger = class {
|
|
|
825
1067
|
}
|
|
826
1068
|
}
|
|
827
1069
|
};
|
|
828
|
-
function createLogger(enableConsole = true, callback, logger) {
|
|
829
|
-
return new MCPLogger(enableConsole, callback, logger);
|
|
1070
|
+
function createLogger(enableConsole = true, callback, logger, logContent = false) {
|
|
1071
|
+
return new MCPLogger(enableConsole, callback, logger, logContent);
|
|
830
1072
|
}
|
|
831
1073
|
|
|
832
1074
|
// src/mcp-server/rate-limiter.ts
|
|
1075
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
833
1076
|
var RateLimiter = class {
|
|
834
1077
|
requestMap;
|
|
835
1078
|
limit;
|
|
@@ -838,9 +1081,9 @@ var RateLimiter = class {
|
|
|
838
1081
|
* Create a new rate limiter
|
|
839
1082
|
*
|
|
840
1083
|
* @param limit - Maximum requests allowed in the time window
|
|
841
|
-
* @param windowMs - Time window in milliseconds (default:
|
|
1084
|
+
* @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
|
|
842
1085
|
*/
|
|
843
|
-
constructor(limit, windowMs =
|
|
1086
|
+
constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
844
1087
|
this.requestMap = /* @__PURE__ */ new Map();
|
|
845
1088
|
this.limit = limit;
|
|
846
1089
|
this.windowMs = windowMs;
|
|
@@ -904,7 +1147,7 @@ var RateLimiter = class {
|
|
|
904
1147
|
return Math.max(0, timeUntilReset);
|
|
905
1148
|
}
|
|
906
1149
|
};
|
|
907
|
-
function
|
|
1150
|
+
function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
908
1151
|
return limit ? new RateLimiter(limit, windowMs) : null;
|
|
909
1152
|
}
|
|
910
1153
|
|
|
@@ -954,11 +1197,15 @@ async function createMCPServer(config) {
|
|
|
954
1197
|
const log = resolveLogger(config.logger, config.logLevel);
|
|
955
1198
|
log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
|
|
956
1199
|
const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
|
|
957
|
-
const
|
|
958
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
1200
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
959
1201
|
const transportMode = config.transport || "stdio";
|
|
960
|
-
const rateLimiter =
|
|
961
|
-
const logger = createLogger(
|
|
1202
|
+
const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
|
|
1203
|
+
const logger = createLogger(
|
|
1204
|
+
config.logging?.console !== false,
|
|
1205
|
+
config.logging?.onChat,
|
|
1206
|
+
log,
|
|
1207
|
+
config.logging?.content === true
|
|
1208
|
+
);
|
|
962
1209
|
const publicToolConfig = {
|
|
963
1210
|
enabled: config.tools?.public?.enabled !== false,
|
|
964
1211
|
name: config.tools?.public?.name || "chat_public",
|
|
@@ -973,13 +1220,22 @@ async function createMCPServer(config) {
|
|
|
973
1220
|
apiKey: config.openai.apiKey
|
|
974
1221
|
});
|
|
975
1222
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1223
|
+
let store;
|
|
1224
|
+
if (config.retriever) {
|
|
1225
|
+
store = config.retriever;
|
|
1226
|
+
} else {
|
|
1227
|
+
if (!config.database) {
|
|
1228
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
1229
|
+
}
|
|
1230
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1231
|
+
const db = await openLibsqlClient2(config.database);
|
|
1232
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
1233
|
+
databaseClient: db,
|
|
1234
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
1235
|
+
logger: log
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
await store.build?.();
|
|
983
1239
|
log.info("\u2705 Knowledge base ready");
|
|
984
1240
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
985
1241
|
const server = new McpServer({
|
|
@@ -1028,6 +1284,7 @@ async function createMCPServer(config) {
|
|
|
1028
1284
|
buckets,
|
|
1029
1285
|
rewriteQuery: config.rewriteQuery,
|
|
1030
1286
|
rerankContext: config.rerankContext,
|
|
1287
|
+
fallbackFn: config.fallbackFn,
|
|
1031
1288
|
logger: log
|
|
1032
1289
|
});
|
|
1033
1290
|
const result = await answerOnce({
|
|
@@ -1117,6 +1374,7 @@ async function createMCPServer(config) {
|
|
|
1117
1374
|
buckets,
|
|
1118
1375
|
rewriteQuery: config.rewriteQuery,
|
|
1119
1376
|
rerankContext: config.rerankContext,
|
|
1377
|
+
fallbackFn: config.fallbackFn,
|
|
1120
1378
|
logger: log
|
|
1121
1379
|
});
|
|
1122
1380
|
const result = await answerOnce({
|
|
@@ -1178,13 +1436,17 @@ async function createMCPServer(config) {
|
|
|
1178
1436
|
return server;
|
|
1179
1437
|
}
|
|
1180
1438
|
|
|
1439
|
+
// src/middleware/apiKey.ts
|
|
1440
|
+
var API_KEY_HEADER = "x-api-key";
|
|
1441
|
+
var API_KEY_PREVIEW_LENGTH = 20;
|
|
1442
|
+
|
|
1181
1443
|
// src/middleware/auth.ts
|
|
1182
1444
|
function createAuthMiddleware(deps) {
|
|
1183
1445
|
const { apiKeyManager, logger } = deps;
|
|
1184
1446
|
return async function requirePublicKey(c, next) {
|
|
1185
|
-
const hdr = c.req.header(
|
|
1186
|
-
if (hdr?.startsWith(
|
|
1187
|
-
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0,
|
|
1447
|
+
const hdr = c.req.header(API_KEY_HEADER);
|
|
1448
|
+
if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1449
|
+
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1188
1450
|
await next();
|
|
1189
1451
|
return;
|
|
1190
1452
|
}
|
|
@@ -1194,7 +1456,7 @@ function createAuthMiddleware(deps) {
|
|
|
1194
1456
|
}
|
|
1195
1457
|
if (!hdr) {
|
|
1196
1458
|
logger.debug("[Auth] No API key provided");
|
|
1197
|
-
return c.json({ error:
|
|
1459
|
+
return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
|
|
1198
1460
|
}
|
|
1199
1461
|
try {
|
|
1200
1462
|
const result = await apiKeyManager.verify(hdr);
|
|
@@ -1208,7 +1470,7 @@ function createAuthMiddleware(deps) {
|
|
|
1208
1470
|
} catch (error) {
|
|
1209
1471
|
logger.warn(`[Auth] JWT verification error: ${error}`);
|
|
1210
1472
|
}
|
|
1211
|
-
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0,
|
|
1473
|
+
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
|
|
1212
1474
|
return c.json({ error: "Unauthorized" }, 401);
|
|
1213
1475
|
};
|
|
1214
1476
|
}
|
|
@@ -1224,7 +1486,7 @@ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message)
|
|
|
1224
1486
|
}
|
|
1225
1487
|
|
|
1226
1488
|
// src/middleware/cors.ts
|
|
1227
|
-
var ALLOWED_HEADERS =
|
|
1489
|
+
var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
|
|
1228
1490
|
var ALLOWED_METHODS = "POST, GET, OPTIONS";
|
|
1229
1491
|
var EXPOSED_HEADERS = "x-conversation-id";
|
|
1230
1492
|
var cors = (allowedOrigins) => async (c, next) => {
|
|
@@ -1340,7 +1602,7 @@ function createWindowBucketLimiter(windowMs, now = Date.now) {
|
|
|
1340
1602
|
}
|
|
1341
1603
|
|
|
1342
1604
|
// src/middleware/ratelimit.ts
|
|
1343
|
-
function
|
|
1605
|
+
function createRateLimiter(config) {
|
|
1344
1606
|
const limiter = createWindowBucketLimiter(6e4);
|
|
1345
1607
|
const publicLimit = config.rateLimit?.public || 60;
|
|
1346
1608
|
const privateLimit = config.rateLimit?.private || 120;
|
|
@@ -1351,7 +1613,7 @@ function createRateLimiter2(config) {
|
|
|
1351
1613
|
// Demo keys get 10x stricter rate limits
|
|
1352
1614
|
limitPublic() {
|
|
1353
1615
|
return async (c, next) => {
|
|
1354
|
-
const apiKey = c.req.header(
|
|
1616
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1355
1617
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1356
1618
|
const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
|
|
1357
1619
|
const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
|
|
@@ -1378,6 +1640,9 @@ function createRateLimiter2(config) {
|
|
|
1378
1640
|
};
|
|
1379
1641
|
}
|
|
1380
1642
|
|
|
1643
|
+
// src/middleware/referrer.ts
|
|
1644
|
+
init_logger();
|
|
1645
|
+
|
|
1381
1646
|
// src/middleware/originMatch.ts
|
|
1382
1647
|
function matchesAllowedOrigin(origin, referer, allowedOrigins) {
|
|
1383
1648
|
return allowedOrigins.some((allowed) => {
|
|
@@ -1408,14 +1673,14 @@ function matchesAllowedOriginOrLocalhost(origin, referer, allowedOrigins) {
|
|
|
1408
1673
|
// src/middleware/referrer.ts
|
|
1409
1674
|
function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
|
|
1410
1675
|
return async (c, next) => {
|
|
1411
|
-
const apiKey = c.req.header(
|
|
1676
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1412
1677
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1413
|
-
const isSessionKey = apiKey?.startsWith(
|
|
1678
|
+
const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
|
|
1414
1679
|
if (isDemoKey || isSessionKey) {
|
|
1415
1680
|
const referer = c.req.header("referer");
|
|
1416
1681
|
const origin = c.req.header("origin");
|
|
1417
1682
|
logger.debug(
|
|
1418
|
-
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0,
|
|
1683
|
+
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
|
|
1419
1684
|
);
|
|
1420
1685
|
logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
|
|
1421
1686
|
const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
|
|
@@ -1458,11 +1723,12 @@ function securityHeaders(config) {
|
|
|
1458
1723
|
}
|
|
1459
1724
|
|
|
1460
1725
|
// src/middleware/session.ts
|
|
1726
|
+
init_logger();
|
|
1461
1727
|
function validateSessionKey(logger = createConsoleLogger()) {
|
|
1462
1728
|
return async (c, next) => {
|
|
1463
|
-
const apiKey = c.req.header(
|
|
1464
|
-
if (apiKey?.startsWith(
|
|
1465
|
-
logger.debug(`[Session] Validating key: ${apiKey.slice(0,
|
|
1729
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1730
|
+
if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1731
|
+
logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1466
1732
|
const info = getSessionInfo(apiKey);
|
|
1467
1733
|
if (info) {
|
|
1468
1734
|
logger.debug(
|
|
@@ -1543,6 +1809,8 @@ function limitDemo(trustProxy) {
|
|
|
1543
1809
|
await next();
|
|
1544
1810
|
};
|
|
1545
1811
|
}
|
|
1812
|
+
var DEMO_SESSION_MAX_REQUESTS = 20;
|
|
1813
|
+
var DEMO_SESSION_TTL_SECONDS = 3600;
|
|
1546
1814
|
function demoRoutes(deps) {
|
|
1547
1815
|
const { client, store, config, prompts, logger } = deps;
|
|
1548
1816
|
const app = new import_hono.Hono();
|
|
@@ -1555,18 +1823,17 @@ function demoRoutes(deps) {
|
|
|
1555
1823
|
limitSessionCreation(trustProxy),
|
|
1556
1824
|
(c) => {
|
|
1557
1825
|
const session = createSession({
|
|
1558
|
-
maxRequests:
|
|
1559
|
-
ttl:
|
|
1560
|
-
// 1 hour
|
|
1826
|
+
maxRequests: DEMO_SESSION_MAX_REQUESTS,
|
|
1827
|
+
ttl: DEMO_SESSION_TTL_SECONDS
|
|
1561
1828
|
});
|
|
1562
1829
|
logger.debug(
|
|
1563
|
-
`[Demo] Created session: ${session.key.slice(0,
|
|
1830
|
+
`[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
|
|
1564
1831
|
);
|
|
1565
1832
|
return c.json({
|
|
1566
1833
|
key: session.key,
|
|
1567
|
-
expiresIn:
|
|
1834
|
+
expiresIn: DEMO_SESSION_TTL_SECONDS,
|
|
1568
1835
|
maxRequests: session.maxRequests,
|
|
1569
|
-
message:
|
|
1836
|
+
message: `Use this temporary key in the ${API_KEY_HEADER} header`
|
|
1570
1837
|
});
|
|
1571
1838
|
}
|
|
1572
1839
|
);
|
|
@@ -1600,6 +1867,7 @@ function demoRoutes(deps) {
|
|
|
1600
1867
|
buckets,
|
|
1601
1868
|
rewriteQuery: config.rewriteQuery,
|
|
1602
1869
|
rerankContext: config.rerankContext,
|
|
1870
|
+
fallbackFn: config.fallbackFn,
|
|
1603
1871
|
logger
|
|
1604
1872
|
});
|
|
1605
1873
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
@@ -1680,7 +1948,7 @@ function oversizedBodyError(message) {
|
|
|
1680
1948
|
function openaiRoutes(deps) {
|
|
1681
1949
|
const { client, store, config, prompts, apiKeyManager, logger } = deps;
|
|
1682
1950
|
const app = new import_hono2.Hono();
|
|
1683
|
-
const { limitPublic, limitPrivate } =
|
|
1951
|
+
const { limitPublic, limitPrivate } = createRateLimiter(config);
|
|
1684
1952
|
const requirePrivateJWT = createJWTMiddleware(config);
|
|
1685
1953
|
const enablePublic = config.features?.enablePublicChat !== false;
|
|
1686
1954
|
const enablePrivate = config.features?.enablePrivateChat !== false;
|
|
@@ -1690,9 +1958,13 @@ function openaiRoutes(deps) {
|
|
|
1690
1958
|
return errorJson(c, 401, "API key authentication is not configured on this server");
|
|
1691
1959
|
}
|
|
1692
1960
|
const bearer = c.req.header("authorization") ?? "";
|
|
1693
|
-
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(
|
|
1961
|
+
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
|
|
1694
1962
|
if (!key) {
|
|
1695
|
-
return errorJson(
|
|
1963
|
+
return errorJson(
|
|
1964
|
+
c,
|
|
1965
|
+
401,
|
|
1966
|
+
`Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
|
|
1967
|
+
);
|
|
1696
1968
|
}
|
|
1697
1969
|
try {
|
|
1698
1970
|
const result = await apiKeyManager.verify(key);
|
|
@@ -1732,6 +2004,7 @@ function openaiRoutes(deps) {
|
|
|
1732
2004
|
sender,
|
|
1733
2005
|
rewriteQuery: config.rewriteQuery,
|
|
1734
2006
|
rerankContext: config.rerankContext,
|
|
2007
|
+
fallbackFn: config.fallbackFn,
|
|
1735
2008
|
logger
|
|
1736
2009
|
}));
|
|
1737
2010
|
} catch {
|
|
@@ -1827,7 +2100,7 @@ function privateRoutes(deps) {
|
|
|
1827
2100
|
const { client, store, config, prompts, logger } = deps;
|
|
1828
2101
|
const app = new import_hono3.Hono();
|
|
1829
2102
|
const requirePrivateJWT = createJWTMiddleware(config);
|
|
1830
|
-
const { limitPrivate } =
|
|
2103
|
+
const { limitPrivate } = createRateLimiter(config);
|
|
1831
2104
|
app.use("/api/private/*", chatBodyLimit(config.server?.maxRequestBytes));
|
|
1832
2105
|
app.use("/api/private/*", requirePrivateJWT);
|
|
1833
2106
|
app.use("/api/private/*", limitPrivate());
|
|
@@ -1855,6 +2128,7 @@ function privateRoutes(deps) {
|
|
|
1855
2128
|
sender,
|
|
1856
2129
|
rewriteQuery: config.rewriteQuery,
|
|
1857
2130
|
rerankContext: config.rerankContext,
|
|
2131
|
+
fallbackFn: config.fallbackFn,
|
|
1858
2132
|
logger
|
|
1859
2133
|
}));
|
|
1860
2134
|
} catch {
|
|
@@ -1912,7 +2186,7 @@ function publicRoutes(deps) {
|
|
|
1912
2186
|
const { client, store, config, prompts, logger } = deps;
|
|
1913
2187
|
const app = new import_hono4.Hono();
|
|
1914
2188
|
const requirePublicKey = createAuthMiddleware(deps);
|
|
1915
|
-
const { limitPublic } =
|
|
2189
|
+
const { limitPublic } = createRateLimiter(config);
|
|
1916
2190
|
const referrerOrigins = config.server?.allowedOrigins?.length ? config.server.allowedOrigins.filter((o) => o !== "*") : [config.bot.publicUrl, "http://localhost:8181", "http://127.0.0.1:8181"];
|
|
1917
2191
|
app.use("/api/public/*", chatBodyLimit(config.server?.maxRequestBytes));
|
|
1918
2192
|
app.use("/api/public/*", validateSessionKey(logger));
|
|
@@ -1940,6 +2214,7 @@ function publicRoutes(deps) {
|
|
|
1940
2214
|
buckets,
|
|
1941
2215
|
rewriteQuery: config.rewriteQuery,
|
|
1942
2216
|
rerankContext: config.rerankContext,
|
|
2217
|
+
fallbackFn: config.fallbackFn,
|
|
1943
2218
|
logger
|
|
1944
2219
|
}));
|
|
1945
2220
|
} catch {
|
|
@@ -1977,11 +2252,11 @@ function publicRoutes(deps) {
|
|
|
1977
2252
|
|
|
1978
2253
|
// src/server.ts
|
|
1979
2254
|
var import_node_path4 = require("node:path");
|
|
1980
|
-
var import_client2 = require("@libsql/client");
|
|
1981
2255
|
var import_hono5 = require("hono");
|
|
1982
2256
|
var import_openai2 = __toESM(require("openai"));
|
|
1983
2257
|
|
|
1984
2258
|
// src/channels/senders.ts
|
|
2259
|
+
init_logger();
|
|
1985
2260
|
function createSenderRegistry(logger = createConsoleLogger()) {
|
|
1986
2261
|
const senders = /* @__PURE__ */ new Map();
|
|
1987
2262
|
return {
|
|
@@ -2044,6 +2319,10 @@ function createSenderRegistry(logger = createConsoleLogger()) {
|
|
|
2044
2319
|
};
|
|
2045
2320
|
}
|
|
2046
2321
|
|
|
2322
|
+
// src/server.ts
|
|
2323
|
+
init_logger();
|
|
2324
|
+
init_retrieval();
|
|
2325
|
+
|
|
2047
2326
|
// src/core/serve-static.ts
|
|
2048
2327
|
var STATIC_ADAPTERS = {
|
|
2049
2328
|
bun: "hono/bun",
|
|
@@ -2105,8 +2384,7 @@ function resolveStatic(configStaticDir) {
|
|
|
2105
2384
|
async function createServer(config) {
|
|
2106
2385
|
const logger = resolveLogger(config.logger, config.logLevel);
|
|
2107
2386
|
logger.info(`\u{1F680} Starting ${config.bot.name}...`);
|
|
2108
|
-
const
|
|
2109
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
2387
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
2110
2388
|
const publicDir = config.publicDir || "./public";
|
|
2111
2389
|
const enablePublic = config.features?.enablePublicChat !== false;
|
|
2112
2390
|
const enablePrivate = config.features?.enablePrivateChat !== false;
|
|
@@ -2117,16 +2395,26 @@ async function createServer(config) {
|
|
|
2117
2395
|
const client = new import_openai2.default({
|
|
2118
2396
|
apiKey: config.openai.apiKey
|
|
2119
2397
|
});
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2398
|
+
let db;
|
|
2399
|
+
if (config.database) {
|
|
2400
|
+
const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
2401
|
+
db = await openLibsqlClient2(config.database);
|
|
2402
|
+
}
|
|
2403
|
+
let store;
|
|
2404
|
+
if (config.retriever) {
|
|
2405
|
+
store = config.retriever;
|
|
2406
|
+
} else {
|
|
2407
|
+
if (!db) {
|
|
2408
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
2409
|
+
}
|
|
2410
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
2411
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
2412
|
+
databaseClient: db,
|
|
2413
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
2414
|
+
logger
|
|
2415
|
+
});
|
|
2416
|
+
}
|
|
2417
|
+
await store.build?.();
|
|
2130
2418
|
logger.info("\u2705 Knowledge base ready");
|
|
2131
2419
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
2132
2420
|
let apiKeyManager;
|
|
@@ -2214,6 +2502,9 @@ async function createServer(config) {
|
|
|
2214
2502
|
const deps = {
|
|
2215
2503
|
client,
|
|
2216
2504
|
store,
|
|
2505
|
+
// See ServerDependencies.db's doc comment: undefined here (cast to
|
|
2506
|
+
// satisfy the still-required type) exactly when config.database was
|
|
2507
|
+
// never set - i.e. a config.retriever host with nothing else needing db.
|
|
2217
2508
|
db,
|
|
2218
2509
|
config,
|
|
2219
2510
|
prompts,
|
|
@@ -2269,13 +2560,16 @@ async function createServer(config) {
|
|
|
2269
2560
|
}
|
|
2270
2561
|
|
|
2271
2562
|
// src/shortener.ts
|
|
2272
|
-
var
|
|
2563
|
+
var SHORTENER_TIMEOUT_MS = 5e3;
|
|
2273
2564
|
function createShortener(config, fetchImpl = fetch) {
|
|
2274
2565
|
const apiUrl = config.apiUrl.replace(/\/+$/, "");
|
|
2275
2566
|
return {
|
|
2276
2567
|
async shorten(url) {
|
|
2277
2568
|
const controller = new AbortController();
|
|
2278
|
-
const timeout = setTimeout(
|
|
2569
|
+
const timeout = setTimeout(
|
|
2570
|
+
() => controller.abort(),
|
|
2571
|
+
config.timeoutMs ?? SHORTENER_TIMEOUT_MS
|
|
2572
|
+
);
|
|
2279
2573
|
try {
|
|
2280
2574
|
const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
|
|
2281
2575
|
method: "POST",
|
|
@@ -2304,12 +2598,17 @@ function createShortener(config, fetchImpl = fetch) {
|
|
|
2304
2598
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2305
2599
|
0 && (module.exports = {
|
|
2306
2600
|
ApiKeyManager,
|
|
2601
|
+
BUILD_LOCK_KEY,
|
|
2602
|
+
BUILD_LOCK_STALE_MS,
|
|
2603
|
+
DEFAULT_KNOWLEDGE_DIR,
|
|
2307
2604
|
DEFAULT_MODEL,
|
|
2605
|
+
DEFAULT_PROMPTS_DIR,
|
|
2308
2606
|
PromptLoader,
|
|
2309
2607
|
VectorStore,
|
|
2310
2608
|
answerOnce,
|
|
2311
2609
|
answerStream,
|
|
2312
2610
|
applyTransformReply,
|
|
2611
|
+
canAcquireBuildLock,
|
|
2313
2612
|
chatBodyLimit,
|
|
2314
2613
|
completeOnce,
|
|
2315
2614
|
completeStream,
|
|
@@ -2318,12 +2617,14 @@ function createShortener(config, fetchImpl = fetch) {
|
|
|
2318
2617
|
createConsoleLogger,
|
|
2319
2618
|
createJWTMiddleware,
|
|
2320
2619
|
createMCPServer,
|
|
2620
|
+
createOpenAIEmbedder,
|
|
2321
2621
|
createRateLimiter,
|
|
2322
2622
|
createReferrerCheck,
|
|
2323
2623
|
createServer,
|
|
2324
2624
|
createSession,
|
|
2325
2625
|
createSessionMiddleware,
|
|
2326
2626
|
createShortener,
|
|
2627
|
+
createTursoBuildLock,
|
|
2327
2628
|
defaultBuckets,
|
|
2328
2629
|
demoRoutes,
|
|
2329
2630
|
detectLeakage,
|