@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/index.mjs
CHANGED
|
@@ -1,3 +1,398 @@
|
|
|
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/buildLock.ts
|
|
17
|
+
function canAcquireBuildLock(existing, holderId, now, staleMs = BUILD_LOCK_STALE_MS) {
|
|
18
|
+
if (!existing) return true;
|
|
19
|
+
if (existing.holderId === holderId) return true;
|
|
20
|
+
return now - existing.heartbeatAt >= staleMs;
|
|
21
|
+
}
|
|
22
|
+
function ensureBuildLockSchema(client) {
|
|
23
|
+
let ready = schemaReady.get(client);
|
|
24
|
+
if (!ready) {
|
|
25
|
+
ready = client.execute(
|
|
26
|
+
`CREATE TABLE IF NOT EXISTS chatter_build_lock (
|
|
27
|
+
lock_key TEXT PRIMARY KEY,
|
|
28
|
+
holder_id TEXT NOT NULL,
|
|
29
|
+
heartbeat_at INTEGER NOT NULL
|
|
30
|
+
)`
|
|
31
|
+
).catch((error) => {
|
|
32
|
+
schemaReady.delete(client);
|
|
33
|
+
throw error;
|
|
34
|
+
});
|
|
35
|
+
schemaReady.set(client, ready);
|
|
36
|
+
}
|
|
37
|
+
return ready;
|
|
38
|
+
}
|
|
39
|
+
function createTursoBuildLock(client) {
|
|
40
|
+
return {
|
|
41
|
+
async tryAcquire(key, holderId, now, staleMs) {
|
|
42
|
+
await ensureBuildLockSchema(client);
|
|
43
|
+
const result = await client.execute({
|
|
44
|
+
sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
|
|
45
|
+
ON CONFLICT(lock_key) DO UPDATE SET
|
|
46
|
+
holder_id = excluded.holder_id,
|
|
47
|
+
heartbeat_at = excluded.heartbeat_at
|
|
48
|
+
WHERE chatter_build_lock.holder_id = excluded.holder_id
|
|
49
|
+
OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
|
|
50
|
+
RETURNING holder_id`,
|
|
51
|
+
args: [key, holderId, now, staleMs]
|
|
52
|
+
});
|
|
53
|
+
const row = result.rows[0];
|
|
54
|
+
return Boolean(row) && row.holder_id === holderId;
|
|
55
|
+
},
|
|
56
|
+
async release(key, holderId) {
|
|
57
|
+
await ensureBuildLockSchema(client);
|
|
58
|
+
await client.execute({
|
|
59
|
+
sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
|
|
60
|
+
args: [key, holderId]
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
|
|
66
|
+
var init_buildLock = __esm({
|
|
67
|
+
"src/core/buildLock.ts"() {
|
|
68
|
+
"use strict";
|
|
69
|
+
BUILD_LOCK_KEY = "knowledge_build";
|
|
70
|
+
BUILD_LOCK_STALE_MS = 10 * 6e4;
|
|
71
|
+
schemaReady = /* @__PURE__ */ new WeakMap();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// src/core/loaders.ts
|
|
76
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
77
|
+
import { join } from "node:path";
|
|
78
|
+
function walk(dir) {
|
|
79
|
+
const items = [];
|
|
80
|
+
try {
|
|
81
|
+
for (const e of readdirSync(dir)) {
|
|
82
|
+
const p = join(dir, e);
|
|
83
|
+
const st = statSync(p);
|
|
84
|
+
if (st.isDirectory()) items.push(...walk(p));
|
|
85
|
+
else if (p.endsWith(".md")) items.push(p);
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
return items;
|
|
90
|
+
}
|
|
91
|
+
function loadKnowledge(root = "config/knowledge") {
|
|
92
|
+
const docs = [];
|
|
93
|
+
for (const bucket of ["base", "public", "private"]) {
|
|
94
|
+
const dir = join(root, bucket);
|
|
95
|
+
for (const f of walk(dir)) {
|
|
96
|
+
docs.push({ id: f, text: readFileSync(f, "utf8"), source: f, bucket });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return docs;
|
|
100
|
+
}
|
|
101
|
+
var init_loaders = __esm({
|
|
102
|
+
"src/core/loaders.ts"() {
|
|
103
|
+
"use strict";
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// src/core/logger.ts
|
|
108
|
+
function createConsoleLogger(level = "info") {
|
|
109
|
+
const threshold = LEVEL_ORDER[level];
|
|
110
|
+
const write = (at) => (...args) => {
|
|
111
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
112
|
+
console.error(`[${at}]`, ...args);
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
debug: write("debug"),
|
|
116
|
+
info: write("info"),
|
|
117
|
+
warn: write("warn"),
|
|
118
|
+
error: write("error")
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function resolveLogger(logger, level) {
|
|
122
|
+
return logger ?? createConsoleLogger(level);
|
|
123
|
+
}
|
|
124
|
+
var LEVEL_ORDER;
|
|
125
|
+
var init_logger = __esm({
|
|
126
|
+
"src/core/logger.ts"() {
|
|
127
|
+
"use strict";
|
|
128
|
+
LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// src/core/retrieval.ts
|
|
133
|
+
var retrieval_exports = {};
|
|
134
|
+
__export(retrieval_exports, {
|
|
135
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
|
|
136
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
137
|
+
VectorStore: () => VectorStore,
|
|
138
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
139
|
+
openLibsqlClient: () => openLibsqlClient,
|
|
140
|
+
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
141
|
+
});
|
|
142
|
+
function createOpenAIEmbedder(client) {
|
|
143
|
+
return async (input) => {
|
|
144
|
+
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
145
|
+
return res.data.map((d) => d.embedding);
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function wrapMissingLibsqlError(cause) {
|
|
149
|
+
return new Error(
|
|
150
|
+
"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.",
|
|
151
|
+
{ cause }
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
async function openLibsqlClient(database) {
|
|
155
|
+
const mod = await import("@libsql/client").catch((error) => {
|
|
156
|
+
throw wrapMissingLibsqlError(error);
|
|
157
|
+
});
|
|
158
|
+
return mod.createClient({ url: database.url, authToken: database.authToken });
|
|
159
|
+
}
|
|
160
|
+
function chunk(text, max = 900) {
|
|
161
|
+
const out = [];
|
|
162
|
+
let buf = "";
|
|
163
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
164
|
+
if (`${buf}
|
|
165
|
+
${line}`.length > max) {
|
|
166
|
+
out.push(buf.trim());
|
|
167
|
+
buf = line;
|
|
168
|
+
} else buf += `
|
|
169
|
+
${line}`;
|
|
170
|
+
}
|
|
171
|
+
if (buf.trim()) out.push(buf.trim());
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
async function sha256(input) {
|
|
175
|
+
const data = new TextEncoder().encode(input);
|
|
176
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
177
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
178
|
+
}
|
|
179
|
+
var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
|
|
180
|
+
var init_retrieval = __esm({
|
|
181
|
+
"src/core/retrieval.ts"() {
|
|
182
|
+
"use strict";
|
|
183
|
+
init_buildLock();
|
|
184
|
+
init_loaders();
|
|
185
|
+
init_logger();
|
|
186
|
+
EMB_MODEL = "text-embedding-3-large";
|
|
187
|
+
DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
|
|
188
|
+
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.";
|
|
189
|
+
VectorStore = class _VectorStore {
|
|
190
|
+
constructor(embed, options) {
|
|
191
|
+
this.embed = embed;
|
|
192
|
+
this.db = options.databaseClient;
|
|
193
|
+
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
194
|
+
this.logger = options.logger ?? createConsoleLogger();
|
|
195
|
+
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
196
|
+
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
197
|
+
this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
|
|
198
|
+
}
|
|
199
|
+
embed;
|
|
200
|
+
/**
|
|
201
|
+
* The libsql client backing this store - the same instance passed in as
|
|
202
|
+
* `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
|
|
203
|
+
* and the store share one connection.
|
|
204
|
+
*/
|
|
205
|
+
db;
|
|
206
|
+
knowledgeDir;
|
|
207
|
+
logger;
|
|
208
|
+
buildLock;
|
|
209
|
+
instanceId;
|
|
210
|
+
buildLockStaleMs;
|
|
211
|
+
/**
|
|
212
|
+
* On boot: ingest new chunks and embed only missing ones.
|
|
213
|
+
*
|
|
214
|
+
* The whole ingest runs under a single-writer lock held in the database
|
|
215
|
+
* (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
|
|
216
|
+
* every chunk id the current `knowledgeDir` did not produce. Without the
|
|
217
|
+
* lock, a second instance booting mid-build diffs against a database the
|
|
218
|
+
* first one is still writing and deletes its chunks. An instance that
|
|
219
|
+
* cannot take the lock skips its build entirely and serves what the holder
|
|
220
|
+
* has already written, rather than racing it.
|
|
221
|
+
*/
|
|
222
|
+
async build() {
|
|
223
|
+
this.logger.info("Building knowledge base...");
|
|
224
|
+
await this.db.execute(`
|
|
225
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
226
|
+
`);
|
|
227
|
+
await this.db.execute(`
|
|
228
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
229
|
+
`);
|
|
230
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
231
|
+
if (!await this.renewBuildLock()) {
|
|
232
|
+
this.logger.warn(
|
|
233
|
+
"Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
await this.buildUnderLock();
|
|
239
|
+
} finally {
|
|
240
|
+
await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
|
|
244
|
+
renewBuildLock() {
|
|
245
|
+
return this.buildLock.tryAcquire(
|
|
246
|
+
BUILD_LOCK_KEY,
|
|
247
|
+
this.instanceId,
|
|
248
|
+
Date.now(),
|
|
249
|
+
this.buildLockStaleMs
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
/** The ingest itself. Only ever runs with the build lock held. */
|
|
253
|
+
async buildUnderLock() {
|
|
254
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
255
|
+
this.logger.info(`Loaded ${docs.length} knowledge documents`);
|
|
256
|
+
if (docs.length === 0) {
|
|
257
|
+
const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
|
|
258
|
+
const existingCount = Number(existing.rows[0]?.count ?? 0);
|
|
259
|
+
if (existingCount > 0) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`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.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const rows = [];
|
|
268
|
+
for (const d of docs) {
|
|
269
|
+
for (const part of chunk(d.text)) {
|
|
270
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
271
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
275
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
276
|
+
const UPSERT_BATCH = 500;
|
|
277
|
+
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
278
|
+
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
279
|
+
await this.db.batch(
|
|
280
|
+
batch.map((r) => ({
|
|
281
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
282
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
283
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
284
|
+
})),
|
|
285
|
+
"write"
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const ids = rows.map((r) => r.id);
|
|
289
|
+
const missing = [];
|
|
290
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
291
|
+
const batch = ids.slice(i, i + 500);
|
|
292
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
293
|
+
const res = await this.db.execute({
|
|
294
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
295
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
296
|
+
args: batch
|
|
297
|
+
});
|
|
298
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
299
|
+
}
|
|
300
|
+
if (missing.length === 0) {
|
|
301
|
+
this.logger.info("No new chunks to embed - knowledge base is up to date");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
305
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
306
|
+
const BATCH = 96;
|
|
307
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
308
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
309
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
310
|
+
const vectors = await this.embed(inputs);
|
|
311
|
+
await this.db.batch(
|
|
312
|
+
vectors.map((embedding, idx) => ({
|
|
313
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
314
|
+
args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
|
|
315
|
+
})),
|
|
316
|
+
"write"
|
|
317
|
+
);
|
|
318
|
+
if (!await this.renewBuildLock()) {
|
|
319
|
+
this.logger.warn(
|
|
320
|
+
"Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
325
|
+
}
|
|
326
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
327
|
+
async cleanupStaleChunks(currentIds) {
|
|
328
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
329
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
330
|
+
const currentIdSet = new Set(currentIds);
|
|
331
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
332
|
+
if (staleIds.length === 0) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
|
|
336
|
+
const BATCH_SIZE = 500;
|
|
337
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
338
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
339
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
340
|
+
await this.db.batch(
|
|
341
|
+
[
|
|
342
|
+
{ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
|
|
343
|
+
{ sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
|
|
344
|
+
],
|
|
345
|
+
"write"
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
349
|
+
}
|
|
350
|
+
static cosine(a, b) {
|
|
351
|
+
let dot = 0;
|
|
352
|
+
let na = 0;
|
|
353
|
+
let nb = 0;
|
|
354
|
+
for (let i = 0; i < a.length; i++) {
|
|
355
|
+
dot += a[i] * b[i];
|
|
356
|
+
na += a[i] * a[i];
|
|
357
|
+
nb += b[i] * b[i];
|
|
358
|
+
}
|
|
359
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
363
|
+
* & portable).
|
|
364
|
+
*
|
|
365
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
366
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
367
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
368
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
369
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
370
|
+
* the embedding call.
|
|
371
|
+
*/
|
|
372
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
373
|
+
if (allowed.length === 0) return [];
|
|
374
|
+
const [qv] = await this.embed([q]);
|
|
375
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
376
|
+
const res = await this.db.execute({
|
|
377
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
378
|
+
FROM chunks c
|
|
379
|
+
JOIN embeddings e ON e.id = c.id
|
|
380
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
381
|
+
args: allowed
|
|
382
|
+
});
|
|
383
|
+
const scored = [];
|
|
384
|
+
for (const row of res.rows) {
|
|
385
|
+
const emb = JSON.parse(String(row.embedding));
|
|
386
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
387
|
+
scored.push({ s, text: String(row.text) });
|
|
388
|
+
}
|
|
389
|
+
scored.sort((a, b) => b.s - a.s);
|
|
390
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
|
|
1
396
|
// src/auth/apikeys.ts
|
|
2
397
|
import { randomUUID } from "node:crypto";
|
|
3
398
|
import { jwtVerify, SignJWT } from "jose";
|
|
@@ -212,7 +607,8 @@ async function answerOnce({
|
|
|
212
607
|
async function applyTransformReply(transformReply, input, logger) {
|
|
213
608
|
if (!transformReply) return input.text;
|
|
214
609
|
try {
|
|
215
|
-
|
|
610
|
+
const result = await transformReply(input);
|
|
611
|
+
return typeof result === "string" ? result : null;
|
|
216
612
|
} catch (error) {
|
|
217
613
|
logger?.error(
|
|
218
614
|
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
@@ -277,51 +673,10 @@ async function resolveBuckets({
|
|
|
277
673
|
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
278
674
|
}
|
|
279
675
|
|
|
280
|
-
// src/
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const items = [];
|
|
285
|
-
try {
|
|
286
|
-
for (const e of readdirSync(dir)) {
|
|
287
|
-
const p = join(dir, e);
|
|
288
|
-
const st = statSync(p);
|
|
289
|
-
if (st.isDirectory()) items.push(...walk(p));
|
|
290
|
-
else if (p.endsWith(".md")) items.push(p);
|
|
291
|
-
}
|
|
292
|
-
} catch {
|
|
293
|
-
}
|
|
294
|
-
return items;
|
|
295
|
-
}
|
|
296
|
-
function loadKnowledge(root = "config/knowledge") {
|
|
297
|
-
const docs = [];
|
|
298
|
-
for (const bucket of ["base", "public", "private"]) {
|
|
299
|
-
const dir = join(root, bucket);
|
|
300
|
-
for (const f of walk(dir)) {
|
|
301
|
-
docs.push({ id: f, text: readFileSync(f, "utf8"), source: f, bucket });
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
return docs;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
// src/core/logger.ts
|
|
308
|
-
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
309
|
-
function createConsoleLogger(level = "info") {
|
|
310
|
-
const threshold = LEVEL_ORDER[level];
|
|
311
|
-
const write = (at) => (...args) => {
|
|
312
|
-
if (LEVEL_ORDER[at] < threshold) return;
|
|
313
|
-
console.error(`[${at}]`, ...args);
|
|
314
|
-
};
|
|
315
|
-
return {
|
|
316
|
-
debug: write("debug"),
|
|
317
|
-
info: write("info"),
|
|
318
|
-
warn: write("warn"),
|
|
319
|
-
error: write("error")
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
function resolveLogger(logger, level) {
|
|
323
|
-
return logger ?? createConsoleLogger(level);
|
|
324
|
-
}
|
|
676
|
+
// src/index.ts
|
|
677
|
+
init_buildLock();
|
|
678
|
+
init_loaders();
|
|
679
|
+
init_logger();
|
|
325
680
|
|
|
326
681
|
// src/core/messages.ts
|
|
327
682
|
function isClientRole(role) {
|
|
@@ -388,6 +743,7 @@ async function prepareChat({
|
|
|
388
743
|
sender,
|
|
389
744
|
rewriteQuery,
|
|
390
745
|
rerankContext,
|
|
746
|
+
fallbackFn,
|
|
391
747
|
logger
|
|
392
748
|
}) {
|
|
393
749
|
const lastUserMsg = lastUserMessage(messages);
|
|
@@ -404,21 +760,39 @@ async function prepareChat({
|
|
|
404
760
|
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
405
761
|
}
|
|
406
762
|
}
|
|
407
|
-
|
|
763
|
+
const resolvedBuckets = buckets ?? defaultBuckets(mode);
|
|
764
|
+
const retrievedChunks = await store.query(query, topK, resolvedBuckets);
|
|
765
|
+
let ctx = retrievedChunks;
|
|
408
766
|
if (rerankContext) {
|
|
409
767
|
try {
|
|
410
|
-
const reranked = await rerankContext({ query, chunks: ctx });
|
|
768
|
+
const reranked = await rerankContext({ query, chunks: [...ctx] });
|
|
411
769
|
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
412
770
|
} catch (error) {
|
|
413
771
|
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
414
772
|
}
|
|
415
773
|
}
|
|
774
|
+
let fallback;
|
|
775
|
+
if (ctx.length === 0 && fallbackFn) {
|
|
776
|
+
try {
|
|
777
|
+
const guidance = await fallbackFn({
|
|
778
|
+
query,
|
|
779
|
+
mode,
|
|
780
|
+
buckets: resolvedBuckets,
|
|
781
|
+
sender,
|
|
782
|
+
retrievedChunks
|
|
783
|
+
});
|
|
784
|
+
if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
|
|
785
|
+
} catch (error) {
|
|
786
|
+
logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
416
789
|
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
417
790
|
const hint = channelHint?.trim();
|
|
418
791
|
const system = [
|
|
419
792
|
prompts.baseSystemRules,
|
|
420
793
|
persona,
|
|
421
794
|
...hint ? [hint] : [],
|
|
795
|
+
...fallback ? [fallback] : [],
|
|
422
796
|
`${label}:
|
|
423
797
|
${ctx.join("\n\n")}`
|
|
424
798
|
].join("\n\n");
|
|
@@ -428,6 +802,7 @@ ${ctx.join("\n\n")}`
|
|
|
428
802
|
// src/core/prompts.ts
|
|
429
803
|
import { readFileSync as readFileSync2 } from "node:fs";
|
|
430
804
|
import { join as join2 } from "node:path";
|
|
805
|
+
var DEFAULT_PROMPTS_DIR = "./config/prompts";
|
|
431
806
|
var PromptLoader = class {
|
|
432
807
|
constructor(promptsDir, bot) {
|
|
433
808
|
this.promptsDir = promptsDir;
|
|
@@ -458,196 +833,32 @@ var PromptLoader = class {
|
|
|
458
833
|
}
|
|
459
834
|
};
|
|
460
835
|
|
|
461
|
-
// src/
|
|
462
|
-
|
|
463
|
-
var EMB_MODEL = "text-embedding-3-large";
|
|
464
|
-
function chunk(text, max = 900) {
|
|
465
|
-
const out = [];
|
|
466
|
-
let buf = "";
|
|
467
|
-
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
468
|
-
if (`${buf}
|
|
469
|
-
${line}`.length > max) {
|
|
470
|
-
out.push(buf.trim());
|
|
471
|
-
buf = line;
|
|
472
|
-
} else buf += `
|
|
473
|
-
${line}`;
|
|
474
|
-
}
|
|
475
|
-
if (buf.trim()) out.push(buf.trim());
|
|
476
|
-
return out;
|
|
477
|
-
}
|
|
478
|
-
async function sha256(input) {
|
|
479
|
-
const data = new TextEncoder().encode(input);
|
|
480
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
481
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
482
|
-
}
|
|
483
|
-
var VectorStore = class _VectorStore {
|
|
484
|
-
constructor(client, options) {
|
|
485
|
-
this.client = client;
|
|
486
|
-
this.db = options.databaseClient ? options.databaseClient : createClient({
|
|
487
|
-
url: options.databaseUrl,
|
|
488
|
-
authToken: options.databaseAuthToken
|
|
489
|
-
});
|
|
490
|
-
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
491
|
-
this.logger = options.logger ?? createConsoleLogger();
|
|
492
|
-
}
|
|
493
|
-
client;
|
|
494
|
-
/**
|
|
495
|
-
* The libsql client backing this store. When a client was injected this is
|
|
496
|
-
* that same instance, so callers holding it (e.g. `ServerDependencies.db`)
|
|
497
|
-
* and the store share one connection.
|
|
498
|
-
*/
|
|
499
|
-
db;
|
|
500
|
-
knowledgeDir;
|
|
501
|
-
logger;
|
|
502
|
-
// On boot: ingest new chunks and embed only missing ones.
|
|
503
|
-
async build() {
|
|
504
|
-
this.logger.info("\u{1F504} Building knowledge base...");
|
|
505
|
-
await this.db.execute(`
|
|
506
|
-
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
507
|
-
`);
|
|
508
|
-
await this.db.execute(`
|
|
509
|
-
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
510
|
-
`);
|
|
511
|
-
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
512
|
-
const docs = loadKnowledge(this.knowledgeDir);
|
|
513
|
-
this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
514
|
-
const rows = [];
|
|
515
|
-
for (const d of docs) {
|
|
516
|
-
for (const part of chunk(d.text)) {
|
|
517
|
-
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
518
|
-
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
522
|
-
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
523
|
-
const UPSERT_BATCH = 500;
|
|
524
|
-
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
525
|
-
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
526
|
-
await this.db.batch(
|
|
527
|
-
batch.map((r) => ({
|
|
528
|
-
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
529
|
-
ON CONFLICT(id) DO NOTHING`,
|
|
530
|
-
args: [r.id, r.bucket, r.source, r.text]
|
|
531
|
-
})),
|
|
532
|
-
"write"
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
const ids = rows.map((r) => r.id);
|
|
536
|
-
const missing = [];
|
|
537
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
538
|
-
const batch = ids.slice(i, i + 500);
|
|
539
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
540
|
-
const res = await this.db.execute({
|
|
541
|
-
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
542
|
-
EXCEPT SELECT id FROM embeddings`,
|
|
543
|
-
args: batch
|
|
544
|
-
});
|
|
545
|
-
for (const row of res.rows) missing.push(String(row.id));
|
|
546
|
-
}
|
|
547
|
-
if (missing.length === 0) {
|
|
548
|
-
this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
552
|
-
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
553
|
-
const BATCH = 96;
|
|
554
|
-
for (let i = 0; i < missing.length; i += BATCH) {
|
|
555
|
-
const batchIds = missing.slice(i, i + BATCH);
|
|
556
|
-
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
557
|
-
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
558
|
-
await this.db.batch(
|
|
559
|
-
emb.data.map((d, idx) => ({
|
|
560
|
-
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
561
|
-
args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
|
|
562
|
-
})),
|
|
563
|
-
"write"
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
567
|
-
}
|
|
568
|
-
// Remove chunks from database that no longer exist in markdown files
|
|
569
|
-
async cleanupStaleChunks(currentIds) {
|
|
570
|
-
const result = await this.db.execute("SELECT id FROM chunks");
|
|
571
|
-
const dbIds = result.rows.map((row) => String(row.id));
|
|
572
|
-
const currentIdSet = new Set(currentIds);
|
|
573
|
-
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
574
|
-
if (staleIds.length === 0) {
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
578
|
-
const BATCH_SIZE = 500;
|
|
579
|
-
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
580
|
-
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
581
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
582
|
-
await this.db.execute({
|
|
583
|
-
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
584
|
-
args: batch
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
588
|
-
}
|
|
589
|
-
static cosine(a, b) {
|
|
590
|
-
let dot = 0;
|
|
591
|
-
let na = 0;
|
|
592
|
-
let nb = 0;
|
|
593
|
-
for (let i = 0; i < a.length; i++) {
|
|
594
|
-
dot += a[i] * b[i];
|
|
595
|
-
na += a[i] * a[i];
|
|
596
|
-
nb += b[i] * b[i];
|
|
597
|
-
}
|
|
598
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
599
|
-
}
|
|
600
|
-
/**
|
|
601
|
-
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
602
|
-
* & portable).
|
|
603
|
-
*
|
|
604
|
-
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
605
|
-
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
606
|
-
* buckets can query them (`build` only writes and prunes the three the
|
|
607
|
-
* knowledge loader walks). Names are bound as query parameters, never
|
|
608
|
-
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
609
|
-
* the embedding call.
|
|
610
|
-
*/
|
|
611
|
-
async query(q, k = 6, allowed = ["base"]) {
|
|
612
|
-
if (allowed.length === 0) return [];
|
|
613
|
-
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
614
|
-
const placeholders = allowed.map(() => "?").join(",");
|
|
615
|
-
const res = await this.db.execute({
|
|
616
|
-
sql: `SELECT c.id, c.text, e.embedding
|
|
617
|
-
FROM chunks c
|
|
618
|
-
JOIN embeddings e ON e.id = c.id
|
|
619
|
-
WHERE c.bucket IN (${placeholders})`,
|
|
620
|
-
args: allowed
|
|
621
|
-
});
|
|
622
|
-
const scored = [];
|
|
623
|
-
for (const row of res.rows) {
|
|
624
|
-
const emb = JSON.parse(String(row.embedding));
|
|
625
|
-
const s = _VectorStore.cosine(qv, emb);
|
|
626
|
-
scored.push({ s, text: String(row.text) });
|
|
627
|
-
}
|
|
628
|
-
scored.sort((a, b) => b.s - a.s);
|
|
629
|
-
return scored.slice(0, k).map((r) => r.text);
|
|
630
|
-
}
|
|
631
|
-
};
|
|
836
|
+
// src/index.ts
|
|
837
|
+
init_retrieval();
|
|
632
838
|
|
|
633
839
|
// src/core/session.ts
|
|
634
840
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
841
|
+
var SESSION_KEY_PREFIX = "session_";
|
|
842
|
+
var DEFAULT_SESSION_MAX_REQUESTS = 20;
|
|
843
|
+
var DEFAULT_SESSION_TTL_SECONDS = 3600;
|
|
844
|
+
var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
635
845
|
var sessions = /* @__PURE__ */ new Map();
|
|
636
|
-
var sessionSweeper = setInterval(
|
|
637
|
-
()
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
sessions.delete(key);
|
|
642
|
-
}
|
|
846
|
+
var sessionSweeper = setInterval(() => {
|
|
847
|
+
const now = Date.now();
|
|
848
|
+
for (const [key, data] of Array.from(sessions.entries())) {
|
|
849
|
+
if (now > data.expiresAt) {
|
|
850
|
+
sessions.delete(key);
|
|
643
851
|
}
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
);
|
|
852
|
+
}
|
|
853
|
+
}, SESSION_SWEEP_INTERVAL_MS);
|
|
647
854
|
sessionSweeper.unref?.();
|
|
648
855
|
function createSession(options = {}) {
|
|
649
|
-
const {
|
|
650
|
-
|
|
856
|
+
const {
|
|
857
|
+
maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
|
|
858
|
+
ttl = DEFAULT_SESSION_TTL_SECONDS,
|
|
859
|
+
metadata
|
|
860
|
+
} = options;
|
|
861
|
+
const key = `${SESSION_KEY_PREFIX}${randomUUID2()}`;
|
|
651
862
|
const now = Date.now();
|
|
652
863
|
const session = {
|
|
653
864
|
key,
|
|
@@ -683,6 +894,8 @@ function getActiveSessions() {
|
|
|
683
894
|
|
|
684
895
|
// src/mcp-server.ts
|
|
685
896
|
import OpenAI from "openai";
|
|
897
|
+
init_logger();
|
|
898
|
+
init_retrieval();
|
|
686
899
|
|
|
687
900
|
// src/mcp-server/conversation-id.ts
|
|
688
901
|
function generateConversationId() {
|
|
@@ -709,28 +922,38 @@ function calculateCost(usage, rates) {
|
|
|
709
922
|
}
|
|
710
923
|
|
|
711
924
|
// src/mcp-server/logger.ts
|
|
925
|
+
init_logger();
|
|
712
926
|
var MCPLogger = class {
|
|
713
927
|
enableConsole;
|
|
714
928
|
callback;
|
|
715
929
|
logger;
|
|
716
|
-
|
|
930
|
+
logContent;
|
|
931
|
+
constructor(enableConsole = true, callback, logger, logContent = false) {
|
|
717
932
|
this.enableConsole = enableConsole;
|
|
718
933
|
this.callback = callback;
|
|
719
934
|
this.logger = logger ?? createConsoleLogger();
|
|
935
|
+
this.logContent = logContent;
|
|
720
936
|
}
|
|
721
937
|
/**
|
|
722
938
|
* Log a chat interaction
|
|
723
939
|
*/
|
|
724
940
|
async logChatInteraction(toolName, conversationId, conversationMessages, ragContext, response, duration, cost) {
|
|
725
941
|
const lastUserMsg = lastUserMessage(conversationMessages);
|
|
942
|
+
const content = {
|
|
943
|
+
userMessage: scrubOutput(lastUserMsg?.content || ""),
|
|
944
|
+
conversationHistory: conversationMessages.map((msg) => ({
|
|
945
|
+
...msg,
|
|
946
|
+
content: scrubOutput(msg.content)
|
|
947
|
+
})),
|
|
948
|
+
ragContext: ragContext.map(scrubOutput),
|
|
949
|
+
response: scrubOutput(response)
|
|
950
|
+
};
|
|
951
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
726
952
|
const logEvent = {
|
|
727
|
-
timestamp
|
|
953
|
+
timestamp,
|
|
728
954
|
toolName,
|
|
729
955
|
conversationId,
|
|
730
|
-
|
|
731
|
-
conversationHistory: conversationMessages,
|
|
732
|
-
ragContext,
|
|
733
|
-
response,
|
|
956
|
+
...content,
|
|
734
957
|
duration,
|
|
735
958
|
cost
|
|
736
959
|
};
|
|
@@ -738,9 +961,27 @@ var MCPLogger = class {
|
|
|
738
961
|
this.logger.info(
|
|
739
962
|
JSON.stringify({
|
|
740
963
|
event: "mcp_chat",
|
|
741
|
-
|
|
964
|
+
timestamp,
|
|
965
|
+
toolName,
|
|
966
|
+
conversationId,
|
|
967
|
+
messageCount: conversationMessages.length,
|
|
968
|
+
ragContextCount: ragContext.length,
|
|
969
|
+
responseChars: response.length,
|
|
970
|
+
duration,
|
|
971
|
+
cost
|
|
742
972
|
})
|
|
743
973
|
);
|
|
974
|
+
if (this.logContent) {
|
|
975
|
+
this.logger.debug(
|
|
976
|
+
JSON.stringify({
|
|
977
|
+
event: "mcp_chat_content",
|
|
978
|
+
timestamp,
|
|
979
|
+
toolName,
|
|
980
|
+
conversationId,
|
|
981
|
+
...content
|
|
982
|
+
})
|
|
983
|
+
);
|
|
984
|
+
}
|
|
744
985
|
}
|
|
745
986
|
if (this.callback) {
|
|
746
987
|
try {
|
|
@@ -751,11 +992,12 @@ var MCPLogger = class {
|
|
|
751
992
|
}
|
|
752
993
|
}
|
|
753
994
|
};
|
|
754
|
-
function createLogger(enableConsole = true, callback, logger) {
|
|
755
|
-
return new MCPLogger(enableConsole, callback, logger);
|
|
995
|
+
function createLogger(enableConsole = true, callback, logger, logContent = false) {
|
|
996
|
+
return new MCPLogger(enableConsole, callback, logger, logContent);
|
|
756
997
|
}
|
|
757
998
|
|
|
758
999
|
// src/mcp-server/rate-limiter.ts
|
|
1000
|
+
var DEFAULT_WINDOW_MS = 6e4;
|
|
759
1001
|
var RateLimiter = class {
|
|
760
1002
|
requestMap;
|
|
761
1003
|
limit;
|
|
@@ -764,9 +1006,9 @@ var RateLimiter = class {
|
|
|
764
1006
|
* Create a new rate limiter
|
|
765
1007
|
*
|
|
766
1008
|
* @param limit - Maximum requests allowed in the time window
|
|
767
|
-
* @param windowMs - Time window in milliseconds (default:
|
|
1009
|
+
* @param windowMs - Time window in milliseconds (default: `DEFAULT_WINDOW_MS`)
|
|
768
1010
|
*/
|
|
769
|
-
constructor(limit, windowMs =
|
|
1011
|
+
constructor(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
770
1012
|
this.requestMap = /* @__PURE__ */ new Map();
|
|
771
1013
|
this.limit = limit;
|
|
772
1014
|
this.windowMs = windowMs;
|
|
@@ -830,7 +1072,7 @@ var RateLimiter = class {
|
|
|
830
1072
|
return Math.max(0, timeUntilReset);
|
|
831
1073
|
}
|
|
832
1074
|
};
|
|
833
|
-
function
|
|
1075
|
+
function createMcpRateLimiter(limit, windowMs = DEFAULT_WINDOW_MS) {
|
|
834
1076
|
return limit ? new RateLimiter(limit, windowMs) : null;
|
|
835
1077
|
}
|
|
836
1078
|
|
|
@@ -880,11 +1122,15 @@ async function createMCPServer(config) {
|
|
|
880
1122
|
const log = resolveLogger(config.logger, config.logLevel);
|
|
881
1123
|
log.info(`\u{1F680} Starting ${config.bot.name} MCP Server...`);
|
|
882
1124
|
const [McpServer, z] = await Promise.all([loadMcpSdk(), loadZod()]);
|
|
883
|
-
const
|
|
884
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
1125
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
885
1126
|
const transportMode = config.transport || "stdio";
|
|
886
|
-
const rateLimiter =
|
|
887
|
-
const logger = createLogger(
|
|
1127
|
+
const rateLimiter = createMcpRateLimiter(config.toolRateLimit);
|
|
1128
|
+
const logger = createLogger(
|
|
1129
|
+
config.logging?.console !== false,
|
|
1130
|
+
config.logging?.onChat,
|
|
1131
|
+
log,
|
|
1132
|
+
config.logging?.content === true
|
|
1133
|
+
);
|
|
888
1134
|
const publicToolConfig = {
|
|
889
1135
|
enabled: config.tools?.public?.enabled !== false,
|
|
890
1136
|
name: config.tools?.public?.name || "chat_public",
|
|
@@ -899,13 +1145,22 @@ async function createMCPServer(config) {
|
|
|
899
1145
|
apiKey: config.openai.apiKey
|
|
900
1146
|
});
|
|
901
1147
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1148
|
+
let store;
|
|
1149
|
+
if (config.retriever) {
|
|
1150
|
+
store = config.retriever;
|
|
1151
|
+
} else {
|
|
1152
|
+
if (!config.database) {
|
|
1153
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
1154
|
+
}
|
|
1155
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1156
|
+
const db = await openLibsqlClient2(config.database);
|
|
1157
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
1158
|
+
databaseClient: db,
|
|
1159
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
1160
|
+
logger: log
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
await store.build?.();
|
|
909
1164
|
log.info("\u2705 Knowledge base ready");
|
|
910
1165
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
911
1166
|
const server = new McpServer({
|
|
@@ -954,6 +1209,7 @@ async function createMCPServer(config) {
|
|
|
954
1209
|
buckets,
|
|
955
1210
|
rewriteQuery: config.rewriteQuery,
|
|
956
1211
|
rerankContext: config.rerankContext,
|
|
1212
|
+
fallbackFn: config.fallbackFn,
|
|
957
1213
|
logger: log
|
|
958
1214
|
});
|
|
959
1215
|
const result = await answerOnce({
|
|
@@ -1043,6 +1299,7 @@ async function createMCPServer(config) {
|
|
|
1043
1299
|
buckets,
|
|
1044
1300
|
rewriteQuery: config.rewriteQuery,
|
|
1045
1301
|
rerankContext: config.rerankContext,
|
|
1302
|
+
fallbackFn: config.fallbackFn,
|
|
1046
1303
|
logger: log
|
|
1047
1304
|
});
|
|
1048
1305
|
const result = await answerOnce({
|
|
@@ -1104,13 +1361,17 @@ async function createMCPServer(config) {
|
|
|
1104
1361
|
return server;
|
|
1105
1362
|
}
|
|
1106
1363
|
|
|
1364
|
+
// src/middleware/apiKey.ts
|
|
1365
|
+
var API_KEY_HEADER = "x-api-key";
|
|
1366
|
+
var API_KEY_PREVIEW_LENGTH = 20;
|
|
1367
|
+
|
|
1107
1368
|
// src/middleware/auth.ts
|
|
1108
1369
|
function createAuthMiddleware(deps) {
|
|
1109
1370
|
const { apiKeyManager, logger } = deps;
|
|
1110
1371
|
return async function requirePublicKey(c, next) {
|
|
1111
|
-
const hdr = c.req.header(
|
|
1112
|
-
if (hdr?.startsWith(
|
|
1113
|
-
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0,
|
|
1372
|
+
const hdr = c.req.header(API_KEY_HEADER);
|
|
1373
|
+
if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1374
|
+
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1114
1375
|
await next();
|
|
1115
1376
|
return;
|
|
1116
1377
|
}
|
|
@@ -1120,7 +1381,7 @@ function createAuthMiddleware(deps) {
|
|
|
1120
1381
|
}
|
|
1121
1382
|
if (!hdr) {
|
|
1122
1383
|
logger.debug("[Auth] No API key provided");
|
|
1123
|
-
return c.json({ error:
|
|
1384
|
+
return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
|
|
1124
1385
|
}
|
|
1125
1386
|
try {
|
|
1126
1387
|
const result = await apiKeyManager.verify(hdr);
|
|
@@ -1134,7 +1395,7 @@ function createAuthMiddleware(deps) {
|
|
|
1134
1395
|
} catch (error) {
|
|
1135
1396
|
logger.warn(`[Auth] JWT verification error: ${error}`);
|
|
1136
1397
|
}
|
|
1137
|
-
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0,
|
|
1398
|
+
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
|
|
1138
1399
|
return c.json({ error: "Unauthorized" }, 401);
|
|
1139
1400
|
};
|
|
1140
1401
|
}
|
|
@@ -1150,7 +1411,7 @@ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message)
|
|
|
1150
1411
|
}
|
|
1151
1412
|
|
|
1152
1413
|
// src/middleware/cors.ts
|
|
1153
|
-
var ALLOWED_HEADERS =
|
|
1414
|
+
var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
|
|
1154
1415
|
var ALLOWED_METHODS = "POST, GET, OPTIONS";
|
|
1155
1416
|
var EXPOSED_HEADERS = "x-conversation-id";
|
|
1156
1417
|
var cors = (allowedOrigins) => async (c, next) => {
|
|
@@ -1266,7 +1527,7 @@ function createWindowBucketLimiter(windowMs, now = Date.now) {
|
|
|
1266
1527
|
}
|
|
1267
1528
|
|
|
1268
1529
|
// src/middleware/ratelimit.ts
|
|
1269
|
-
function
|
|
1530
|
+
function createRateLimiter(config) {
|
|
1270
1531
|
const limiter = createWindowBucketLimiter(6e4);
|
|
1271
1532
|
const publicLimit = config.rateLimit?.public || 60;
|
|
1272
1533
|
const privateLimit = config.rateLimit?.private || 120;
|
|
@@ -1277,7 +1538,7 @@ function createRateLimiter2(config) {
|
|
|
1277
1538
|
// Demo keys get 10x stricter rate limits
|
|
1278
1539
|
limitPublic() {
|
|
1279
1540
|
return async (c, next) => {
|
|
1280
|
-
const apiKey = c.req.header(
|
|
1541
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1281
1542
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1282
1543
|
const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
|
|
1283
1544
|
const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
|
|
@@ -1304,6 +1565,9 @@ function createRateLimiter2(config) {
|
|
|
1304
1565
|
};
|
|
1305
1566
|
}
|
|
1306
1567
|
|
|
1568
|
+
// src/middleware/referrer.ts
|
|
1569
|
+
init_logger();
|
|
1570
|
+
|
|
1307
1571
|
// src/middleware/originMatch.ts
|
|
1308
1572
|
function matchesAllowedOrigin(origin, referer, allowedOrigins) {
|
|
1309
1573
|
return allowedOrigins.some((allowed) => {
|
|
@@ -1334,14 +1598,14 @@ function matchesAllowedOriginOrLocalhost(origin, referer, allowedOrigins) {
|
|
|
1334
1598
|
// src/middleware/referrer.ts
|
|
1335
1599
|
function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
|
|
1336
1600
|
return async (c, next) => {
|
|
1337
|
-
const apiKey = c.req.header(
|
|
1601
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1338
1602
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1339
|
-
const isSessionKey = apiKey?.startsWith(
|
|
1603
|
+
const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
|
|
1340
1604
|
if (isDemoKey || isSessionKey) {
|
|
1341
1605
|
const referer = c.req.header("referer");
|
|
1342
1606
|
const origin = c.req.header("origin");
|
|
1343
1607
|
logger.debug(
|
|
1344
|
-
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0,
|
|
1608
|
+
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
|
|
1345
1609
|
);
|
|
1346
1610
|
logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
|
|
1347
1611
|
const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
|
|
@@ -1384,11 +1648,12 @@ function securityHeaders(config) {
|
|
|
1384
1648
|
}
|
|
1385
1649
|
|
|
1386
1650
|
// src/middleware/session.ts
|
|
1651
|
+
init_logger();
|
|
1387
1652
|
function validateSessionKey(logger = createConsoleLogger()) {
|
|
1388
1653
|
return async (c, next) => {
|
|
1389
|
-
const apiKey = c.req.header(
|
|
1390
|
-
if (apiKey?.startsWith(
|
|
1391
|
-
logger.debug(`[Session] Validating key: ${apiKey.slice(0,
|
|
1654
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1655
|
+
if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1656
|
+
logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1392
1657
|
const info = getSessionInfo(apiKey);
|
|
1393
1658
|
if (info) {
|
|
1394
1659
|
logger.debug(
|
|
@@ -1469,6 +1734,8 @@ function limitDemo(trustProxy) {
|
|
|
1469
1734
|
await next();
|
|
1470
1735
|
};
|
|
1471
1736
|
}
|
|
1737
|
+
var DEMO_SESSION_MAX_REQUESTS = 20;
|
|
1738
|
+
var DEMO_SESSION_TTL_SECONDS = 3600;
|
|
1472
1739
|
function demoRoutes(deps) {
|
|
1473
1740
|
const { client, store, config, prompts, logger } = deps;
|
|
1474
1741
|
const app = new Hono();
|
|
@@ -1481,18 +1748,17 @@ function demoRoutes(deps) {
|
|
|
1481
1748
|
limitSessionCreation(trustProxy),
|
|
1482
1749
|
(c) => {
|
|
1483
1750
|
const session = createSession({
|
|
1484
|
-
maxRequests:
|
|
1485
|
-
ttl:
|
|
1486
|
-
// 1 hour
|
|
1751
|
+
maxRequests: DEMO_SESSION_MAX_REQUESTS,
|
|
1752
|
+
ttl: DEMO_SESSION_TTL_SECONDS
|
|
1487
1753
|
});
|
|
1488
1754
|
logger.debug(
|
|
1489
|
-
`[Demo] Created session: ${session.key.slice(0,
|
|
1755
|
+
`[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
|
|
1490
1756
|
);
|
|
1491
1757
|
return c.json({
|
|
1492
1758
|
key: session.key,
|
|
1493
|
-
expiresIn:
|
|
1759
|
+
expiresIn: DEMO_SESSION_TTL_SECONDS,
|
|
1494
1760
|
maxRequests: session.maxRequests,
|
|
1495
|
-
message:
|
|
1761
|
+
message: `Use this temporary key in the ${API_KEY_HEADER} header`
|
|
1496
1762
|
});
|
|
1497
1763
|
}
|
|
1498
1764
|
);
|
|
@@ -1526,6 +1792,7 @@ function demoRoutes(deps) {
|
|
|
1526
1792
|
buckets,
|
|
1527
1793
|
rewriteQuery: config.rewriteQuery,
|
|
1528
1794
|
rerankContext: config.rerankContext,
|
|
1795
|
+
fallbackFn: config.fallbackFn,
|
|
1529
1796
|
logger
|
|
1530
1797
|
});
|
|
1531
1798
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
@@ -1606,7 +1873,7 @@ function oversizedBodyError(message) {
|
|
|
1606
1873
|
function openaiRoutes(deps) {
|
|
1607
1874
|
const { client, store, config, prompts, apiKeyManager, logger } = deps;
|
|
1608
1875
|
const app = new Hono2();
|
|
1609
|
-
const { limitPublic, limitPrivate } =
|
|
1876
|
+
const { limitPublic, limitPrivate } = createRateLimiter(config);
|
|
1610
1877
|
const requirePrivateJWT = createJWTMiddleware(config);
|
|
1611
1878
|
const enablePublic = config.features?.enablePublicChat !== false;
|
|
1612
1879
|
const enablePrivate = config.features?.enablePrivateChat !== false;
|
|
@@ -1616,9 +1883,13 @@ function openaiRoutes(deps) {
|
|
|
1616
1883
|
return errorJson(c, 401, "API key authentication is not configured on this server");
|
|
1617
1884
|
}
|
|
1618
1885
|
const bearer = c.req.header("authorization") ?? "";
|
|
1619
|
-
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(
|
|
1886
|
+
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
|
|
1620
1887
|
if (!key) {
|
|
1621
|
-
return errorJson(
|
|
1888
|
+
return errorJson(
|
|
1889
|
+
c,
|
|
1890
|
+
401,
|
|
1891
|
+
`Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
|
|
1892
|
+
);
|
|
1622
1893
|
}
|
|
1623
1894
|
try {
|
|
1624
1895
|
const result = await apiKeyManager.verify(key);
|
|
@@ -1658,6 +1929,7 @@ function openaiRoutes(deps) {
|
|
|
1658
1929
|
sender,
|
|
1659
1930
|
rewriteQuery: config.rewriteQuery,
|
|
1660
1931
|
rerankContext: config.rerankContext,
|
|
1932
|
+
fallbackFn: config.fallbackFn,
|
|
1661
1933
|
logger
|
|
1662
1934
|
}));
|
|
1663
1935
|
} catch {
|
|
@@ -1753,7 +2025,7 @@ function privateRoutes(deps) {
|
|
|
1753
2025
|
const { client, store, config, prompts, logger } = deps;
|
|
1754
2026
|
const app = new Hono3();
|
|
1755
2027
|
const requirePrivateJWT = createJWTMiddleware(config);
|
|
1756
|
-
const { limitPrivate } =
|
|
2028
|
+
const { limitPrivate } = createRateLimiter(config);
|
|
1757
2029
|
app.use("/api/private/*", chatBodyLimit(config.server?.maxRequestBytes));
|
|
1758
2030
|
app.use("/api/private/*", requirePrivateJWT);
|
|
1759
2031
|
app.use("/api/private/*", limitPrivate());
|
|
@@ -1781,6 +2053,7 @@ function privateRoutes(deps) {
|
|
|
1781
2053
|
sender,
|
|
1782
2054
|
rewriteQuery: config.rewriteQuery,
|
|
1783
2055
|
rerankContext: config.rerankContext,
|
|
2056
|
+
fallbackFn: config.fallbackFn,
|
|
1784
2057
|
logger
|
|
1785
2058
|
}));
|
|
1786
2059
|
} catch {
|
|
@@ -1838,7 +2111,7 @@ function publicRoutes(deps) {
|
|
|
1838
2111
|
const { client, store, config, prompts, logger } = deps;
|
|
1839
2112
|
const app = new Hono4();
|
|
1840
2113
|
const requirePublicKey = createAuthMiddleware(deps);
|
|
1841
|
-
const { limitPublic } =
|
|
2114
|
+
const { limitPublic } = createRateLimiter(config);
|
|
1842
2115
|
const referrerOrigins = config.server?.allowedOrigins?.length ? config.server.allowedOrigins.filter((o) => o !== "*") : [config.bot.publicUrl, "http://localhost:8181", "http://127.0.0.1:8181"];
|
|
1843
2116
|
app.use("/api/public/*", chatBodyLimit(config.server?.maxRequestBytes));
|
|
1844
2117
|
app.use("/api/public/*", validateSessionKey(logger));
|
|
@@ -1866,6 +2139,7 @@ function publicRoutes(deps) {
|
|
|
1866
2139
|
buckets,
|
|
1867
2140
|
rewriteQuery: config.rewriteQuery,
|
|
1868
2141
|
rerankContext: config.rerankContext,
|
|
2142
|
+
fallbackFn: config.fallbackFn,
|
|
1869
2143
|
logger
|
|
1870
2144
|
}));
|
|
1871
2145
|
} catch {
|
|
@@ -1903,11 +2177,11 @@ function publicRoutes(deps) {
|
|
|
1903
2177
|
|
|
1904
2178
|
// src/server.ts
|
|
1905
2179
|
import { relative } from "node:path";
|
|
1906
|
-
import { createClient as createClient2 } from "@libsql/client";
|
|
1907
2180
|
import { Hono as Hono5 } from "hono";
|
|
1908
2181
|
import OpenAI2 from "openai";
|
|
1909
2182
|
|
|
1910
2183
|
// src/channels/senders.ts
|
|
2184
|
+
init_logger();
|
|
1911
2185
|
function createSenderRegistry(logger = createConsoleLogger()) {
|
|
1912
2186
|
const senders = /* @__PURE__ */ new Map();
|
|
1913
2187
|
return {
|
|
@@ -1970,6 +2244,10 @@ function createSenderRegistry(logger = createConsoleLogger()) {
|
|
|
1970
2244
|
};
|
|
1971
2245
|
}
|
|
1972
2246
|
|
|
2247
|
+
// src/server.ts
|
|
2248
|
+
init_logger();
|
|
2249
|
+
init_retrieval();
|
|
2250
|
+
|
|
1973
2251
|
// src/core/serve-static.ts
|
|
1974
2252
|
var STATIC_ADAPTERS = {
|
|
1975
2253
|
bun: "hono/bun",
|
|
@@ -2030,8 +2308,7 @@ function resolveStatic(configStaticDir) {
|
|
|
2030
2308
|
async function createServer(config) {
|
|
2031
2309
|
const logger = resolveLogger(config.logger, config.logLevel);
|
|
2032
2310
|
logger.info(`\u{1F680} Starting ${config.bot.name}...`);
|
|
2033
|
-
const
|
|
2034
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
2311
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
2035
2312
|
const publicDir = config.publicDir || "./public";
|
|
2036
2313
|
const enablePublic = config.features?.enablePublicChat !== false;
|
|
2037
2314
|
const enablePrivate = config.features?.enablePrivateChat !== false;
|
|
@@ -2042,16 +2319,26 @@ async function createServer(config) {
|
|
|
2042
2319
|
const client = new OpenAI2({
|
|
2043
2320
|
apiKey: config.openai.apiKey
|
|
2044
2321
|
});
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2322
|
+
let db;
|
|
2323
|
+
if (config.database) {
|
|
2324
|
+
const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
2325
|
+
db = await openLibsqlClient2(config.database);
|
|
2326
|
+
}
|
|
2327
|
+
let store;
|
|
2328
|
+
if (config.retriever) {
|
|
2329
|
+
store = config.retriever;
|
|
2330
|
+
} else {
|
|
2331
|
+
if (!db) {
|
|
2332
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
2333
|
+
}
|
|
2334
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
2335
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
2336
|
+
databaseClient: db,
|
|
2337
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
2338
|
+
logger
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
await store.build?.();
|
|
2055
2342
|
logger.info("\u2705 Knowledge base ready");
|
|
2056
2343
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
2057
2344
|
let apiKeyManager;
|
|
@@ -2139,6 +2426,9 @@ async function createServer(config) {
|
|
|
2139
2426
|
const deps = {
|
|
2140
2427
|
client,
|
|
2141
2428
|
store,
|
|
2429
|
+
// See ServerDependencies.db's doc comment: undefined here (cast to
|
|
2430
|
+
// satisfy the still-required type) exactly when config.database was
|
|
2431
|
+
// never set - i.e. a config.retriever host with nothing else needing db.
|
|
2142
2432
|
db,
|
|
2143
2433
|
config,
|
|
2144
2434
|
prompts,
|
|
@@ -2194,13 +2484,16 @@ async function createServer(config) {
|
|
|
2194
2484
|
}
|
|
2195
2485
|
|
|
2196
2486
|
// src/shortener.ts
|
|
2197
|
-
var
|
|
2487
|
+
var SHORTENER_TIMEOUT_MS = 5e3;
|
|
2198
2488
|
function createShortener(config, fetchImpl = fetch) {
|
|
2199
2489
|
const apiUrl = config.apiUrl.replace(/\/+$/, "");
|
|
2200
2490
|
return {
|
|
2201
2491
|
async shorten(url) {
|
|
2202
2492
|
const controller = new AbortController();
|
|
2203
|
-
const timeout = setTimeout(
|
|
2493
|
+
const timeout = setTimeout(
|
|
2494
|
+
() => controller.abort(),
|
|
2495
|
+
config.timeoutMs ?? SHORTENER_TIMEOUT_MS
|
|
2496
|
+
);
|
|
2204
2497
|
try {
|
|
2205
2498
|
const response = await fetchImpl(`${apiUrl}/api/v2/links`, {
|
|
2206
2499
|
method: "POST",
|
|
@@ -2228,12 +2521,17 @@ function createShortener(config, fetchImpl = fetch) {
|
|
|
2228
2521
|
}
|
|
2229
2522
|
export {
|
|
2230
2523
|
ApiKeyManager,
|
|
2524
|
+
BUILD_LOCK_KEY,
|
|
2525
|
+
BUILD_LOCK_STALE_MS,
|
|
2526
|
+
DEFAULT_KNOWLEDGE_DIR,
|
|
2231
2527
|
DEFAULT_MODEL,
|
|
2528
|
+
DEFAULT_PROMPTS_DIR,
|
|
2232
2529
|
PromptLoader,
|
|
2233
2530
|
VectorStore,
|
|
2234
2531
|
answerOnce,
|
|
2235
2532
|
answerStream,
|
|
2236
2533
|
applyTransformReply,
|
|
2534
|
+
canAcquireBuildLock,
|
|
2237
2535
|
chatBodyLimit,
|
|
2238
2536
|
completeOnce,
|
|
2239
2537
|
completeStream,
|
|
@@ -2242,12 +2540,14 @@ export {
|
|
|
2242
2540
|
createConsoleLogger,
|
|
2243
2541
|
createJWTMiddleware,
|
|
2244
2542
|
createMCPServer,
|
|
2245
|
-
|
|
2543
|
+
createOpenAIEmbedder,
|
|
2544
|
+
createRateLimiter,
|
|
2246
2545
|
createReferrerCheck,
|
|
2247
2546
|
createServer,
|
|
2248
2547
|
createSession,
|
|
2249
2548
|
createSessionMiddleware,
|
|
2250
2549
|
createShortener,
|
|
2550
|
+
createTursoBuildLock,
|
|
2251
2551
|
defaultBuckets,
|
|
2252
2552
|
demoRoutes,
|
|
2253
2553
|
detectLeakage,
|