@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/server.mjs
CHANGED
|
@@ -1,6 +1,395 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res, err) => function __init() {
|
|
4
|
+
if (err) throw err[0];
|
|
5
|
+
try {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
} catch (e) {
|
|
8
|
+
throw err = [e], e;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/core/logger.ts
|
|
17
|
+
function createConsoleLogger(level = "info") {
|
|
18
|
+
const threshold = LEVEL_ORDER[level];
|
|
19
|
+
const write = (at) => (...args) => {
|
|
20
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
21
|
+
console.error(`[${at}]`, ...args);
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
debug: write("debug"),
|
|
25
|
+
info: write("info"),
|
|
26
|
+
warn: write("warn"),
|
|
27
|
+
error: write("error")
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function resolveLogger(logger, level) {
|
|
31
|
+
return logger ?? createConsoleLogger(level);
|
|
32
|
+
}
|
|
33
|
+
var LEVEL_ORDER;
|
|
34
|
+
var init_logger = __esm({
|
|
35
|
+
"src/core/logger.ts"() {
|
|
36
|
+
"use strict";
|
|
37
|
+
LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// src/core/buildLock.ts
|
|
42
|
+
function ensureBuildLockSchema(client) {
|
|
43
|
+
let ready = schemaReady.get(client);
|
|
44
|
+
if (!ready) {
|
|
45
|
+
ready = client.execute(
|
|
46
|
+
`CREATE TABLE IF NOT EXISTS chatter_build_lock (
|
|
47
|
+
lock_key TEXT PRIMARY KEY,
|
|
48
|
+
holder_id TEXT NOT NULL,
|
|
49
|
+
heartbeat_at INTEGER NOT NULL
|
|
50
|
+
)`
|
|
51
|
+
).catch((error) => {
|
|
52
|
+
schemaReady.delete(client);
|
|
53
|
+
throw error;
|
|
54
|
+
});
|
|
55
|
+
schemaReady.set(client, ready);
|
|
56
|
+
}
|
|
57
|
+
return ready;
|
|
58
|
+
}
|
|
59
|
+
function createTursoBuildLock(client) {
|
|
60
|
+
return {
|
|
61
|
+
async tryAcquire(key, holderId, now, staleMs) {
|
|
62
|
+
await ensureBuildLockSchema(client);
|
|
63
|
+
const result = await client.execute({
|
|
64
|
+
sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
|
|
65
|
+
ON CONFLICT(lock_key) DO UPDATE SET
|
|
66
|
+
holder_id = excluded.holder_id,
|
|
67
|
+
heartbeat_at = excluded.heartbeat_at
|
|
68
|
+
WHERE chatter_build_lock.holder_id = excluded.holder_id
|
|
69
|
+
OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
|
|
70
|
+
RETURNING holder_id`,
|
|
71
|
+
args: [key, holderId, now, staleMs]
|
|
72
|
+
});
|
|
73
|
+
const row = result.rows[0];
|
|
74
|
+
return Boolean(row) && row.holder_id === holderId;
|
|
75
|
+
},
|
|
76
|
+
async release(key, holderId) {
|
|
77
|
+
await ensureBuildLockSchema(client);
|
|
78
|
+
await client.execute({
|
|
79
|
+
sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
|
|
80
|
+
args: [key, holderId]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
|
|
86
|
+
var init_buildLock = __esm({
|
|
87
|
+
"src/core/buildLock.ts"() {
|
|
88
|
+
"use strict";
|
|
89
|
+
BUILD_LOCK_KEY = "knowledge_build";
|
|
90
|
+
BUILD_LOCK_STALE_MS = 10 * 6e4;
|
|
91
|
+
schemaReady = /* @__PURE__ */ new WeakMap();
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// src/core/loaders.ts
|
|
96
|
+
import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
97
|
+
import { join as join2 } from "node:path";
|
|
98
|
+
function walk(dir) {
|
|
99
|
+
const items = [];
|
|
100
|
+
try {
|
|
101
|
+
for (const e of readdirSync(dir)) {
|
|
102
|
+
const p = join2(dir, e);
|
|
103
|
+
const st = statSync(p);
|
|
104
|
+
if (st.isDirectory()) items.push(...walk(p));
|
|
105
|
+
else if (p.endsWith(".md")) items.push(p);
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
function loadKnowledge(root = "config/knowledge") {
|
|
112
|
+
const docs = [];
|
|
113
|
+
for (const bucket of ["base", "public", "private"]) {
|
|
114
|
+
const dir = join2(root, bucket);
|
|
115
|
+
for (const f of walk(dir)) {
|
|
116
|
+
docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return docs;
|
|
120
|
+
}
|
|
121
|
+
var init_loaders = __esm({
|
|
122
|
+
"src/core/loaders.ts"() {
|
|
123
|
+
"use strict";
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// src/core/retrieval.ts
|
|
128
|
+
var retrieval_exports = {};
|
|
129
|
+
__export(retrieval_exports, {
|
|
130
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
|
|
131
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
132
|
+
VectorStore: () => VectorStore,
|
|
133
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
134
|
+
openLibsqlClient: () => openLibsqlClient,
|
|
135
|
+
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
136
|
+
});
|
|
137
|
+
function createOpenAIEmbedder(client) {
|
|
138
|
+
return async (input) => {
|
|
139
|
+
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
140
|
+
return res.data.map((d) => d.embedding);
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function wrapMissingLibsqlError(cause) {
|
|
144
|
+
return new Error(
|
|
145
|
+
"Chatter's default knowledge store needs the optional peer dependency '@libsql/client', which is not installed. Install it with `bun add @libsql/client` (or npm/pnpm/yarn), or set config.retriever to use your own retrieval backend instead.",
|
|
146
|
+
{ cause }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
async function openLibsqlClient(database) {
|
|
150
|
+
const mod = await import("@libsql/client").catch((error) => {
|
|
151
|
+
throw wrapMissingLibsqlError(error);
|
|
152
|
+
});
|
|
153
|
+
return mod.createClient({ url: database.url, authToken: database.authToken });
|
|
154
|
+
}
|
|
155
|
+
function chunk(text, max = 900) {
|
|
156
|
+
const out = [];
|
|
157
|
+
let buf = "";
|
|
158
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
159
|
+
if (`${buf}
|
|
160
|
+
${line}`.length > max) {
|
|
161
|
+
out.push(buf.trim());
|
|
162
|
+
buf = line;
|
|
163
|
+
} else buf += `
|
|
164
|
+
${line}`;
|
|
165
|
+
}
|
|
166
|
+
if (buf.trim()) out.push(buf.trim());
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
async function sha256(input) {
|
|
170
|
+
const data = new TextEncoder().encode(input);
|
|
171
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
172
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
173
|
+
}
|
|
174
|
+
var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
|
|
175
|
+
var init_retrieval = __esm({
|
|
176
|
+
"src/core/retrieval.ts"() {
|
|
177
|
+
"use strict";
|
|
178
|
+
init_buildLock();
|
|
179
|
+
init_loaders();
|
|
180
|
+
init_logger();
|
|
181
|
+
EMB_MODEL = "text-embedding-3-large";
|
|
182
|
+
DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
|
|
183
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE = "config.database is required unless config.retriever is set - Chatter's default knowledge store (VectorStore) needs a Turso/libsql connection. Set config.database, or supply config.retriever to use your own retrieval backend instead.";
|
|
184
|
+
VectorStore = class _VectorStore {
|
|
185
|
+
constructor(embed, options) {
|
|
186
|
+
this.embed = embed;
|
|
187
|
+
this.db = options.databaseClient;
|
|
188
|
+
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
189
|
+
this.logger = options.logger ?? createConsoleLogger();
|
|
190
|
+
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
191
|
+
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
192
|
+
this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
|
|
193
|
+
}
|
|
194
|
+
embed;
|
|
195
|
+
/**
|
|
196
|
+
* The libsql client backing this store - the same instance passed in as
|
|
197
|
+
* `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
|
|
198
|
+
* and the store share one connection.
|
|
199
|
+
*/
|
|
200
|
+
db;
|
|
201
|
+
knowledgeDir;
|
|
202
|
+
logger;
|
|
203
|
+
buildLock;
|
|
204
|
+
instanceId;
|
|
205
|
+
buildLockStaleMs;
|
|
206
|
+
/**
|
|
207
|
+
* On boot: ingest new chunks and embed only missing ones.
|
|
208
|
+
*
|
|
209
|
+
* The whole ingest runs under a single-writer lock held in the database
|
|
210
|
+
* (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
|
|
211
|
+
* every chunk id the current `knowledgeDir` did not produce. Without the
|
|
212
|
+
* lock, a second instance booting mid-build diffs against a database the
|
|
213
|
+
* first one is still writing and deletes its chunks. An instance that
|
|
214
|
+
* cannot take the lock skips its build entirely and serves what the holder
|
|
215
|
+
* has already written, rather than racing it.
|
|
216
|
+
*/
|
|
217
|
+
async build() {
|
|
218
|
+
this.logger.info("Building knowledge base...");
|
|
219
|
+
await this.db.execute(`
|
|
220
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
221
|
+
`);
|
|
222
|
+
await this.db.execute(`
|
|
223
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
224
|
+
`);
|
|
225
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
226
|
+
if (!await this.renewBuildLock()) {
|
|
227
|
+
this.logger.warn(
|
|
228
|
+
"Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
await this.buildUnderLock();
|
|
234
|
+
} finally {
|
|
235
|
+
await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
|
|
239
|
+
renewBuildLock() {
|
|
240
|
+
return this.buildLock.tryAcquire(
|
|
241
|
+
BUILD_LOCK_KEY,
|
|
242
|
+
this.instanceId,
|
|
243
|
+
Date.now(),
|
|
244
|
+
this.buildLockStaleMs
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
/** The ingest itself. Only ever runs with the build lock held. */
|
|
248
|
+
async buildUnderLock() {
|
|
249
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
250
|
+
this.logger.info(`Loaded ${docs.length} knowledge documents`);
|
|
251
|
+
if (docs.length === 0) {
|
|
252
|
+
const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
|
|
253
|
+
const existingCount = Number(existing.rows[0]?.count ?? 0);
|
|
254
|
+
if (existingCount > 0) {
|
|
255
|
+
throw new Error(
|
|
256
|
+
`Refusing to build: loaded 0 knowledge documents from "${this.knowledgeDir}", but ${existingCount} chunks already exist. Proceeding would delete all of them as stale. Check that knowledgeDir resolves to the right path.`
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const rows = [];
|
|
263
|
+
for (const d of docs) {
|
|
264
|
+
for (const part of chunk(d.text)) {
|
|
265
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
266
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
270
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
271
|
+
const UPSERT_BATCH = 500;
|
|
272
|
+
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
273
|
+
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
274
|
+
await this.db.batch(
|
|
275
|
+
batch.map((r) => ({
|
|
276
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
277
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
278
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
279
|
+
})),
|
|
280
|
+
"write"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
const ids = rows.map((r) => r.id);
|
|
284
|
+
const missing = [];
|
|
285
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
286
|
+
const batch = ids.slice(i, i + 500);
|
|
287
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
288
|
+
const res = await this.db.execute({
|
|
289
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
290
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
291
|
+
args: batch
|
|
292
|
+
});
|
|
293
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
294
|
+
}
|
|
295
|
+
if (missing.length === 0) {
|
|
296
|
+
this.logger.info("No new chunks to embed - knowledge base is up to date");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
300
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
301
|
+
const BATCH = 96;
|
|
302
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
303
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
304
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
305
|
+
const vectors = await this.embed(inputs);
|
|
306
|
+
await this.db.batch(
|
|
307
|
+
vectors.map((embedding, idx) => ({
|
|
308
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
309
|
+
args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
|
|
310
|
+
})),
|
|
311
|
+
"write"
|
|
312
|
+
);
|
|
313
|
+
if (!await this.renewBuildLock()) {
|
|
314
|
+
this.logger.warn(
|
|
315
|
+
"Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
320
|
+
}
|
|
321
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
322
|
+
async cleanupStaleChunks(currentIds) {
|
|
323
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
324
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
325
|
+
const currentIdSet = new Set(currentIds);
|
|
326
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
327
|
+
if (staleIds.length === 0) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
|
|
331
|
+
const BATCH_SIZE = 500;
|
|
332
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
333
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
334
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
335
|
+
await this.db.batch(
|
|
336
|
+
[
|
|
337
|
+
{ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
|
|
338
|
+
{ sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
|
|
339
|
+
],
|
|
340
|
+
"write"
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
344
|
+
}
|
|
345
|
+
static cosine(a, b) {
|
|
346
|
+
let dot = 0;
|
|
347
|
+
let na = 0;
|
|
348
|
+
let nb = 0;
|
|
349
|
+
for (let i = 0; i < a.length; i++) {
|
|
350
|
+
dot += a[i] * b[i];
|
|
351
|
+
na += a[i] * a[i];
|
|
352
|
+
nb += b[i] * b[i];
|
|
353
|
+
}
|
|
354
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
358
|
+
* & portable).
|
|
359
|
+
*
|
|
360
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
361
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
362
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
363
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
364
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
365
|
+
* the embedding call.
|
|
366
|
+
*/
|
|
367
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
368
|
+
if (allowed.length === 0) return [];
|
|
369
|
+
const [qv] = await this.embed([q]);
|
|
370
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
371
|
+
const res = await this.db.execute({
|
|
372
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
373
|
+
FROM chunks c
|
|
374
|
+
JOIN embeddings e ON e.id = c.id
|
|
375
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
376
|
+
args: allowed
|
|
377
|
+
});
|
|
378
|
+
const scored = [];
|
|
379
|
+
for (const row of res.rows) {
|
|
380
|
+
const emb = JSON.parse(String(row.embedding));
|
|
381
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
382
|
+
scored.push({ s, text: String(row.text) });
|
|
383
|
+
}
|
|
384
|
+
scored.sort((a, b) => b.s - a.s);
|
|
385
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
1
391
|
// src/server.ts
|
|
2
392
|
import { relative } from "node:path";
|
|
3
|
-
import { createClient as createClient2 } from "@libsql/client";
|
|
4
393
|
import { Hono as Hono5 } from "hono";
|
|
5
394
|
import OpenAI from "openai";
|
|
6
395
|
|
|
@@ -218,7 +607,8 @@ async function answerOnce({
|
|
|
218
607
|
async function applyTransformReply(transformReply, input, logger) {
|
|
219
608
|
if (!transformReply) return input.text;
|
|
220
609
|
try {
|
|
221
|
-
|
|
610
|
+
const result = await transformReply(input);
|
|
611
|
+
return typeof result === "string" ? result : null;
|
|
222
612
|
} catch (error) {
|
|
223
613
|
logger?.error(
|
|
224
614
|
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
@@ -348,6 +738,7 @@ async function prepareChat({
|
|
|
348
738
|
sender,
|
|
349
739
|
rewriteQuery,
|
|
350
740
|
rerankContext,
|
|
741
|
+
fallbackFn,
|
|
351
742
|
logger
|
|
352
743
|
}) {
|
|
353
744
|
const lastUserMsg = lastUserMessage(messages);
|
|
@@ -364,47 +755,47 @@ async function prepareChat({
|
|
|
364
755
|
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
365
756
|
}
|
|
366
757
|
}
|
|
367
|
-
|
|
758
|
+
const resolvedBuckets = buckets ?? defaultBuckets(mode);
|
|
759
|
+
const retrievedChunks = await store.query(query, topK, resolvedBuckets);
|
|
760
|
+
let ctx = retrievedChunks;
|
|
368
761
|
if (rerankContext) {
|
|
369
762
|
try {
|
|
370
|
-
const reranked = await rerankContext({ query, chunks: ctx });
|
|
763
|
+
const reranked = await rerankContext({ query, chunks: [...ctx] });
|
|
371
764
|
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
372
765
|
} catch (error) {
|
|
373
766
|
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
374
767
|
}
|
|
375
768
|
}
|
|
769
|
+
let fallback;
|
|
770
|
+
if (ctx.length === 0 && fallbackFn) {
|
|
771
|
+
try {
|
|
772
|
+
const guidance = await fallbackFn({
|
|
773
|
+
query,
|
|
774
|
+
mode,
|
|
775
|
+
buckets: resolvedBuckets,
|
|
776
|
+
sender,
|
|
777
|
+
retrievedChunks
|
|
778
|
+
});
|
|
779
|
+
if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
|
|
780
|
+
} catch (error) {
|
|
781
|
+
logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
376
784
|
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
377
785
|
const hint = channelHint?.trim();
|
|
378
786
|
const system = [
|
|
379
787
|
prompts.baseSystemRules,
|
|
380
788
|
persona,
|
|
381
789
|
...hint ? [hint] : [],
|
|
790
|
+
...fallback ? [fallback] : [],
|
|
382
791
|
`${label}:
|
|
383
792
|
${ctx.join("\n\n")}`
|
|
384
793
|
].join("\n\n");
|
|
385
794
|
return { system, messages, context: ctx };
|
|
386
795
|
}
|
|
387
796
|
|
|
388
|
-
// src/core/logger.ts
|
|
389
|
-
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
390
|
-
function createConsoleLogger(level = "info") {
|
|
391
|
-
const threshold = LEVEL_ORDER[level];
|
|
392
|
-
const write = (at) => (...args) => {
|
|
393
|
-
if (LEVEL_ORDER[at] < threshold) return;
|
|
394
|
-
console.error(`[${at}]`, ...args);
|
|
395
|
-
};
|
|
396
|
-
return {
|
|
397
|
-
debug: write("debug"),
|
|
398
|
-
info: write("info"),
|
|
399
|
-
warn: write("warn"),
|
|
400
|
-
error: write("error")
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
function resolveLogger(logger, level) {
|
|
404
|
-
return logger ?? createConsoleLogger(level);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
797
|
// src/channels/senders.ts
|
|
798
|
+
init_logger();
|
|
408
799
|
function createSenderRegistry(logger = createConsoleLogger()) {
|
|
409
800
|
const senders = /* @__PURE__ */ new Map();
|
|
410
801
|
return {
|
|
@@ -467,9 +858,13 @@ function createSenderRegistry(logger = createConsoleLogger()) {
|
|
|
467
858
|
};
|
|
468
859
|
}
|
|
469
860
|
|
|
861
|
+
// src/server.ts
|
|
862
|
+
init_logger();
|
|
863
|
+
|
|
470
864
|
// src/core/prompts.ts
|
|
471
865
|
import { readFileSync } from "node:fs";
|
|
472
866
|
import { join } from "node:path";
|
|
867
|
+
var DEFAULT_PROMPTS_DIR = "./config/prompts";
|
|
473
868
|
var PromptLoader = class {
|
|
474
869
|
constructor(promptsDir, bot) {
|
|
475
870
|
this.promptsDir = promptsDir;
|
|
@@ -500,206 +895,8 @@ var PromptLoader = class {
|
|
|
500
895
|
}
|
|
501
896
|
};
|
|
502
897
|
|
|
503
|
-
// src/
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// src/core/loaders.ts
|
|
507
|
-
import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
|
|
508
|
-
import { join as join2 } from "node:path";
|
|
509
|
-
function walk(dir) {
|
|
510
|
-
const items = [];
|
|
511
|
-
try {
|
|
512
|
-
for (const e of readdirSync(dir)) {
|
|
513
|
-
const p = join2(dir, e);
|
|
514
|
-
const st = statSync(p);
|
|
515
|
-
if (st.isDirectory()) items.push(...walk(p));
|
|
516
|
-
else if (p.endsWith(".md")) items.push(p);
|
|
517
|
-
}
|
|
518
|
-
} catch {
|
|
519
|
-
}
|
|
520
|
-
return items;
|
|
521
|
-
}
|
|
522
|
-
function loadKnowledge(root = "config/knowledge") {
|
|
523
|
-
const docs = [];
|
|
524
|
-
for (const bucket of ["base", "public", "private"]) {
|
|
525
|
-
const dir = join2(root, bucket);
|
|
526
|
-
for (const f of walk(dir)) {
|
|
527
|
-
docs.push({ id: f, text: readFileSync2(f, "utf8"), source: f, bucket });
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return docs;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// src/core/retrieval.ts
|
|
534
|
-
var EMB_MODEL = "text-embedding-3-large";
|
|
535
|
-
function chunk(text, max = 900) {
|
|
536
|
-
const out = [];
|
|
537
|
-
let buf = "";
|
|
538
|
-
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
539
|
-
if (`${buf}
|
|
540
|
-
${line}`.length > max) {
|
|
541
|
-
out.push(buf.trim());
|
|
542
|
-
buf = line;
|
|
543
|
-
} else buf += `
|
|
544
|
-
${line}`;
|
|
545
|
-
}
|
|
546
|
-
if (buf.trim()) out.push(buf.trim());
|
|
547
|
-
return out;
|
|
548
|
-
}
|
|
549
|
-
async function sha256(input) {
|
|
550
|
-
const data = new TextEncoder().encode(input);
|
|
551
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
552
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
553
|
-
}
|
|
554
|
-
var VectorStore = class _VectorStore {
|
|
555
|
-
constructor(client, options) {
|
|
556
|
-
this.client = client;
|
|
557
|
-
this.db = options.databaseClient ? options.databaseClient : createClient({
|
|
558
|
-
url: options.databaseUrl,
|
|
559
|
-
authToken: options.databaseAuthToken
|
|
560
|
-
});
|
|
561
|
-
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
562
|
-
this.logger = options.logger ?? createConsoleLogger();
|
|
563
|
-
}
|
|
564
|
-
client;
|
|
565
|
-
/**
|
|
566
|
-
* The libsql client backing this store. When a client was injected this is
|
|
567
|
-
* that same instance, so callers holding it (e.g. `ServerDependencies.db`)
|
|
568
|
-
* and the store share one connection.
|
|
569
|
-
*/
|
|
570
|
-
db;
|
|
571
|
-
knowledgeDir;
|
|
572
|
-
logger;
|
|
573
|
-
// On boot: ingest new chunks and embed only missing ones.
|
|
574
|
-
async build() {
|
|
575
|
-
this.logger.info("\u{1F504} Building knowledge base...");
|
|
576
|
-
await this.db.execute(`
|
|
577
|
-
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
578
|
-
`);
|
|
579
|
-
await this.db.execute(`
|
|
580
|
-
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
581
|
-
`);
|
|
582
|
-
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
583
|
-
const docs = loadKnowledge(this.knowledgeDir);
|
|
584
|
-
this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
585
|
-
const rows = [];
|
|
586
|
-
for (const d of docs) {
|
|
587
|
-
for (const part of chunk(d.text)) {
|
|
588
|
-
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
589
|
-
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
593
|
-
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
594
|
-
const UPSERT_BATCH = 500;
|
|
595
|
-
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
596
|
-
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
597
|
-
await this.db.batch(
|
|
598
|
-
batch.map((r) => ({
|
|
599
|
-
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
600
|
-
ON CONFLICT(id) DO NOTHING`,
|
|
601
|
-
args: [r.id, r.bucket, r.source, r.text]
|
|
602
|
-
})),
|
|
603
|
-
"write"
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
const ids = rows.map((r) => r.id);
|
|
607
|
-
const missing = [];
|
|
608
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
609
|
-
const batch = ids.slice(i, i + 500);
|
|
610
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
611
|
-
const res = await this.db.execute({
|
|
612
|
-
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
613
|
-
EXCEPT SELECT id FROM embeddings`,
|
|
614
|
-
args: batch
|
|
615
|
-
});
|
|
616
|
-
for (const row of res.rows) missing.push(String(row.id));
|
|
617
|
-
}
|
|
618
|
-
if (missing.length === 0) {
|
|
619
|
-
this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
623
|
-
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
624
|
-
const BATCH = 96;
|
|
625
|
-
for (let i = 0; i < missing.length; i += BATCH) {
|
|
626
|
-
const batchIds = missing.slice(i, i + BATCH);
|
|
627
|
-
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
628
|
-
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
629
|
-
await this.db.batch(
|
|
630
|
-
emb.data.map((d, idx) => ({
|
|
631
|
-
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
632
|
-
args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
|
|
633
|
-
})),
|
|
634
|
-
"write"
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
638
|
-
}
|
|
639
|
-
// Remove chunks from database that no longer exist in markdown files
|
|
640
|
-
async cleanupStaleChunks(currentIds) {
|
|
641
|
-
const result = await this.db.execute("SELECT id FROM chunks");
|
|
642
|
-
const dbIds = result.rows.map((row) => String(row.id));
|
|
643
|
-
const currentIdSet = new Set(currentIds);
|
|
644
|
-
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
645
|
-
if (staleIds.length === 0) {
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
649
|
-
const BATCH_SIZE = 500;
|
|
650
|
-
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
651
|
-
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
652
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
653
|
-
await this.db.execute({
|
|
654
|
-
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
655
|
-
args: batch
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
659
|
-
}
|
|
660
|
-
static cosine(a, b) {
|
|
661
|
-
let dot = 0;
|
|
662
|
-
let na = 0;
|
|
663
|
-
let nb = 0;
|
|
664
|
-
for (let i = 0; i < a.length; i++) {
|
|
665
|
-
dot += a[i] * b[i];
|
|
666
|
-
na += a[i] * a[i];
|
|
667
|
-
nb += b[i] * b[i];
|
|
668
|
-
}
|
|
669
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
673
|
-
* & portable).
|
|
674
|
-
*
|
|
675
|
-
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
676
|
-
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
677
|
-
* buckets can query them (`build` only writes and prunes the three the
|
|
678
|
-
* knowledge loader walks). Names are bound as query parameters, never
|
|
679
|
-
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
680
|
-
* the embedding call.
|
|
681
|
-
*/
|
|
682
|
-
async query(q, k = 6, allowed = ["base"]) {
|
|
683
|
-
if (allowed.length === 0) return [];
|
|
684
|
-
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
685
|
-
const placeholders = allowed.map(() => "?").join(",");
|
|
686
|
-
const res = await this.db.execute({
|
|
687
|
-
sql: `SELECT c.id, c.text, e.embedding
|
|
688
|
-
FROM chunks c
|
|
689
|
-
JOIN embeddings e ON e.id = c.id
|
|
690
|
-
WHERE c.bucket IN (${placeholders})`,
|
|
691
|
-
args: allowed
|
|
692
|
-
});
|
|
693
|
-
const scored = [];
|
|
694
|
-
for (const row of res.rows) {
|
|
695
|
-
const emb = JSON.parse(String(row.embedding));
|
|
696
|
-
const s = _VectorStore.cosine(qv, emb);
|
|
697
|
-
scored.push({ s, text: String(row.text) });
|
|
698
|
-
}
|
|
699
|
-
scored.sort((a, b) => b.s - a.s);
|
|
700
|
-
return scored.slice(0, k).map((r) => r.text);
|
|
701
|
-
}
|
|
702
|
-
};
|
|
898
|
+
// src/server.ts
|
|
899
|
+
init_retrieval();
|
|
703
900
|
|
|
704
901
|
// src/core/serve-static.ts
|
|
705
902
|
var STATIC_ADAPTERS = {
|
|
@@ -757,8 +954,12 @@ function resolveStatic(configStaticDir) {
|
|
|
757
954
|
return { staticDir: null };
|
|
758
955
|
}
|
|
759
956
|
|
|
957
|
+
// src/middleware/apiKey.ts
|
|
958
|
+
var API_KEY_HEADER = "x-api-key";
|
|
959
|
+
var API_KEY_PREVIEW_LENGTH = 20;
|
|
960
|
+
|
|
760
961
|
// src/middleware/cors.ts
|
|
761
|
-
var ALLOWED_HEADERS =
|
|
962
|
+
var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
|
|
762
963
|
var ALLOWED_METHODS = "POST, GET, OPTIONS";
|
|
763
964
|
var EXPOSED_HEADERS = "x-conversation-id";
|
|
764
965
|
var cors = (allowedOrigins) => async (c, next) => {
|
|
@@ -822,22 +1023,27 @@ import { stream } from "hono/streaming";
|
|
|
822
1023
|
|
|
823
1024
|
// src/core/session.ts
|
|
824
1025
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
1026
|
+
var SESSION_KEY_PREFIX = "session_";
|
|
1027
|
+
var DEFAULT_SESSION_MAX_REQUESTS = 20;
|
|
1028
|
+
var DEFAULT_SESSION_TTL_SECONDS = 3600;
|
|
1029
|
+
var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
825
1030
|
var sessions = /* @__PURE__ */ new Map();
|
|
826
|
-
var sessionSweeper = setInterval(
|
|
827
|
-
()
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
sessions.delete(key);
|
|
832
|
-
}
|
|
1031
|
+
var sessionSweeper = setInterval(() => {
|
|
1032
|
+
const now = Date.now();
|
|
1033
|
+
for (const [key, data] of Array.from(sessions.entries())) {
|
|
1034
|
+
if (now > data.expiresAt) {
|
|
1035
|
+
sessions.delete(key);
|
|
833
1036
|
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
);
|
|
1037
|
+
}
|
|
1038
|
+
}, SESSION_SWEEP_INTERVAL_MS);
|
|
837
1039
|
sessionSweeper.unref?.();
|
|
838
1040
|
function createSession(options = {}) {
|
|
839
|
-
const {
|
|
840
|
-
|
|
1041
|
+
const {
|
|
1042
|
+
maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
|
|
1043
|
+
ttl = DEFAULT_SESSION_TTL_SECONDS,
|
|
1044
|
+
metadata
|
|
1045
|
+
} = options;
|
|
1046
|
+
const key = `${SESSION_KEY_PREFIX}${randomUUID2()}`;
|
|
841
1047
|
const now = Date.now();
|
|
842
1048
|
const session = {
|
|
843
1049
|
key,
|
|
@@ -1000,6 +1206,8 @@ function limitDemo(trustProxy) {
|
|
|
1000
1206
|
await next();
|
|
1001
1207
|
};
|
|
1002
1208
|
}
|
|
1209
|
+
var DEMO_SESSION_MAX_REQUESTS = 20;
|
|
1210
|
+
var DEMO_SESSION_TTL_SECONDS = 3600;
|
|
1003
1211
|
function demoRoutes(deps) {
|
|
1004
1212
|
const { client, store, config, prompts, logger } = deps;
|
|
1005
1213
|
const app = new Hono();
|
|
@@ -1012,18 +1220,17 @@ function demoRoutes(deps) {
|
|
|
1012
1220
|
limitSessionCreation(trustProxy),
|
|
1013
1221
|
(c) => {
|
|
1014
1222
|
const session = createSession({
|
|
1015
|
-
maxRequests:
|
|
1016
|
-
ttl:
|
|
1017
|
-
// 1 hour
|
|
1223
|
+
maxRequests: DEMO_SESSION_MAX_REQUESTS,
|
|
1224
|
+
ttl: DEMO_SESSION_TTL_SECONDS
|
|
1018
1225
|
});
|
|
1019
1226
|
logger.debug(
|
|
1020
|
-
`[Demo] Created session: ${session.key.slice(0,
|
|
1227
|
+
`[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
|
|
1021
1228
|
);
|
|
1022
1229
|
return c.json({
|
|
1023
1230
|
key: session.key,
|
|
1024
|
-
expiresIn:
|
|
1231
|
+
expiresIn: DEMO_SESSION_TTL_SECONDS,
|
|
1025
1232
|
maxRequests: session.maxRequests,
|
|
1026
|
-
message:
|
|
1233
|
+
message: `Use this temporary key in the ${API_KEY_HEADER} header`
|
|
1027
1234
|
});
|
|
1028
1235
|
}
|
|
1029
1236
|
);
|
|
@@ -1057,6 +1264,7 @@ function demoRoutes(deps) {
|
|
|
1057
1264
|
buckets,
|
|
1058
1265
|
rewriteQuery: config.rewriteQuery,
|
|
1059
1266
|
rerankContext: config.rerankContext,
|
|
1267
|
+
fallbackFn: config.fallbackFn,
|
|
1060
1268
|
logger
|
|
1061
1269
|
});
|
|
1062
1270
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
@@ -1159,7 +1367,7 @@ function createRateLimiter(config) {
|
|
|
1159
1367
|
// Demo keys get 10x stricter rate limits
|
|
1160
1368
|
limitPublic() {
|
|
1161
1369
|
return async (c, next) => {
|
|
1162
|
-
const apiKey = c.req.header(
|
|
1370
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1163
1371
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1164
1372
|
const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
|
|
1165
1373
|
const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
|
|
@@ -1228,9 +1436,13 @@ function openaiRoutes(deps) {
|
|
|
1228
1436
|
return errorJson(c, 401, "API key authentication is not configured on this server");
|
|
1229
1437
|
}
|
|
1230
1438
|
const bearer = c.req.header("authorization") ?? "";
|
|
1231
|
-
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(
|
|
1439
|
+
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
|
|
1232
1440
|
if (!key) {
|
|
1233
|
-
return errorJson(
|
|
1441
|
+
return errorJson(
|
|
1442
|
+
c,
|
|
1443
|
+
401,
|
|
1444
|
+
`Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
|
|
1445
|
+
);
|
|
1234
1446
|
}
|
|
1235
1447
|
try {
|
|
1236
1448
|
const result = await apiKeyManager.verify(key);
|
|
@@ -1270,6 +1482,7 @@ function openaiRoutes(deps) {
|
|
|
1270
1482
|
sender,
|
|
1271
1483
|
rewriteQuery: config.rewriteQuery,
|
|
1272
1484
|
rerankContext: config.rerankContext,
|
|
1485
|
+
fallbackFn: config.fallbackFn,
|
|
1273
1486
|
logger
|
|
1274
1487
|
}));
|
|
1275
1488
|
} catch {
|
|
@@ -1393,6 +1606,7 @@ function privateRoutes(deps) {
|
|
|
1393
1606
|
sender,
|
|
1394
1607
|
rewriteQuery: config.rewriteQuery,
|
|
1395
1608
|
rerankContext: config.rerankContext,
|
|
1609
|
+
fallbackFn: config.fallbackFn,
|
|
1396
1610
|
logger
|
|
1397
1611
|
}));
|
|
1398
1612
|
} catch {
|
|
@@ -1445,9 +1659,9 @@ import { stream as stream4 } from "hono/streaming";
|
|
|
1445
1659
|
function createAuthMiddleware(deps) {
|
|
1446
1660
|
const { apiKeyManager, logger } = deps;
|
|
1447
1661
|
return async function requirePublicKey(c, next) {
|
|
1448
|
-
const hdr = c.req.header(
|
|
1449
|
-
if (hdr?.startsWith(
|
|
1450
|
-
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0,
|
|
1662
|
+
const hdr = c.req.header(API_KEY_HEADER);
|
|
1663
|
+
if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1664
|
+
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1451
1665
|
await next();
|
|
1452
1666
|
return;
|
|
1453
1667
|
}
|
|
@@ -1457,7 +1671,7 @@ function createAuthMiddleware(deps) {
|
|
|
1457
1671
|
}
|
|
1458
1672
|
if (!hdr) {
|
|
1459
1673
|
logger.debug("[Auth] No API key provided");
|
|
1460
|
-
return c.json({ error:
|
|
1674
|
+
return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
|
|
1461
1675
|
}
|
|
1462
1676
|
try {
|
|
1463
1677
|
const result = await apiKeyManager.verify(hdr);
|
|
@@ -1471,22 +1685,23 @@ function createAuthMiddleware(deps) {
|
|
|
1471
1685
|
} catch (error) {
|
|
1472
1686
|
logger.warn(`[Auth] JWT verification error: ${error}`);
|
|
1473
1687
|
}
|
|
1474
|
-
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0,
|
|
1688
|
+
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
|
|
1475
1689
|
return c.json({ error: "Unauthorized" }, 401);
|
|
1476
1690
|
};
|
|
1477
1691
|
}
|
|
1478
1692
|
|
|
1479
1693
|
// src/middleware/referrer.ts
|
|
1694
|
+
init_logger();
|
|
1480
1695
|
function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
|
|
1481
1696
|
return async (c, next) => {
|
|
1482
|
-
const apiKey = c.req.header(
|
|
1697
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1483
1698
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1484
|
-
const isSessionKey = apiKey?.startsWith(
|
|
1699
|
+
const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
|
|
1485
1700
|
if (isDemoKey || isSessionKey) {
|
|
1486
1701
|
const referer = c.req.header("referer");
|
|
1487
1702
|
const origin = c.req.header("origin");
|
|
1488
1703
|
logger.debug(
|
|
1489
|
-
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0,
|
|
1704
|
+
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
|
|
1490
1705
|
);
|
|
1491
1706
|
logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
|
|
1492
1707
|
const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
|
|
@@ -1506,11 +1721,12 @@ function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsol
|
|
|
1506
1721
|
}
|
|
1507
1722
|
|
|
1508
1723
|
// src/middleware/session.ts
|
|
1724
|
+
init_logger();
|
|
1509
1725
|
function validateSessionKey(logger = createConsoleLogger()) {
|
|
1510
1726
|
return async (c, next) => {
|
|
1511
|
-
const apiKey = c.req.header(
|
|
1512
|
-
if (apiKey?.startsWith(
|
|
1513
|
-
logger.debug(`[Session] Validating key: ${apiKey.slice(0,
|
|
1727
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1728
|
+
if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1729
|
+
logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1514
1730
|
const info = getSessionInfo(apiKey);
|
|
1515
1731
|
if (info) {
|
|
1516
1732
|
logger.debug(
|
|
@@ -1574,6 +1790,7 @@ function publicRoutes(deps) {
|
|
|
1574
1790
|
buckets,
|
|
1575
1791
|
rewriteQuery: config.rewriteQuery,
|
|
1576
1792
|
rerankContext: config.rerankContext,
|
|
1793
|
+
fallbackFn: config.fallbackFn,
|
|
1577
1794
|
logger
|
|
1578
1795
|
}));
|
|
1579
1796
|
} catch {
|
|
@@ -1613,8 +1830,7 @@ function publicRoutes(deps) {
|
|
|
1613
1830
|
async function createServer(config) {
|
|
1614
1831
|
const logger = resolveLogger(config.logger, config.logLevel);
|
|
1615
1832
|
logger.info(`\u{1F680} Starting ${config.bot.name}...`);
|
|
1616
|
-
const
|
|
1617
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
1833
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
1618
1834
|
const publicDir = config.publicDir || "./public";
|
|
1619
1835
|
const enablePublic = config.features?.enablePublicChat !== false;
|
|
1620
1836
|
const enablePrivate = config.features?.enablePrivateChat !== false;
|
|
@@ -1625,16 +1841,26 @@ async function createServer(config) {
|
|
|
1625
1841
|
const client = new OpenAI({
|
|
1626
1842
|
apiKey: config.openai.apiKey
|
|
1627
1843
|
});
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1844
|
+
let db;
|
|
1845
|
+
if (config.database) {
|
|
1846
|
+
const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1847
|
+
db = await openLibsqlClient2(config.database);
|
|
1848
|
+
}
|
|
1849
|
+
let store;
|
|
1850
|
+
if (config.retriever) {
|
|
1851
|
+
store = config.retriever;
|
|
1852
|
+
} else {
|
|
1853
|
+
if (!db) {
|
|
1854
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
1855
|
+
}
|
|
1856
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1857
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
1858
|
+
databaseClient: db,
|
|
1859
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
1860
|
+
logger
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
await store.build?.();
|
|
1638
1864
|
logger.info("\u2705 Knowledge base ready");
|
|
1639
1865
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
1640
1866
|
let apiKeyManager;
|
|
@@ -1722,6 +1948,9 @@ async function createServer(config) {
|
|
|
1722
1948
|
const deps = {
|
|
1723
1949
|
client,
|
|
1724
1950
|
store,
|
|
1951
|
+
// See ServerDependencies.db's doc comment: undefined here (cast to
|
|
1952
|
+
// satisfy the still-required type) exactly when config.database was
|
|
1953
|
+
// never set - i.e. a config.retriever host with nothing else needing db.
|
|
1725
1954
|
db,
|
|
1726
1955
|
config,
|
|
1727
1956
|
prompts,
|