@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.js
CHANGED
|
@@ -5,6 +5,14 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res, err) => function __init() {
|
|
9
|
+
if (err) throw err[0];
|
|
10
|
+
try {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
} catch (e) {
|
|
13
|
+
throw err = [e], e;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
8
16
|
var __export = (target, all) => {
|
|
9
17
|
for (var name in all)
|
|
10
18
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
@@ -27,6 +35,382 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
35
|
));
|
|
28
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
37
|
|
|
38
|
+
// src/core/logger.ts
|
|
39
|
+
function createConsoleLogger(level = "info") {
|
|
40
|
+
const threshold = LEVEL_ORDER[level];
|
|
41
|
+
const write = (at) => (...args) => {
|
|
42
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
43
|
+
console.error(`[${at}]`, ...args);
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
debug: write("debug"),
|
|
47
|
+
info: write("info"),
|
|
48
|
+
warn: write("warn"),
|
|
49
|
+
error: write("error")
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function resolveLogger(logger, level) {
|
|
53
|
+
return logger ?? createConsoleLogger(level);
|
|
54
|
+
}
|
|
55
|
+
var LEVEL_ORDER;
|
|
56
|
+
var init_logger = __esm({
|
|
57
|
+
"src/core/logger.ts"() {
|
|
58
|
+
"use strict";
|
|
59
|
+
LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// src/core/buildLock.ts
|
|
64
|
+
function ensureBuildLockSchema(client) {
|
|
65
|
+
let ready = schemaReady.get(client);
|
|
66
|
+
if (!ready) {
|
|
67
|
+
ready = client.execute(
|
|
68
|
+
`CREATE TABLE IF NOT EXISTS chatter_build_lock (
|
|
69
|
+
lock_key TEXT PRIMARY KEY,
|
|
70
|
+
holder_id TEXT NOT NULL,
|
|
71
|
+
heartbeat_at INTEGER NOT NULL
|
|
72
|
+
)`
|
|
73
|
+
).catch((error) => {
|
|
74
|
+
schemaReady.delete(client);
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
schemaReady.set(client, ready);
|
|
78
|
+
}
|
|
79
|
+
return ready;
|
|
80
|
+
}
|
|
81
|
+
function createTursoBuildLock(client) {
|
|
82
|
+
return {
|
|
83
|
+
async tryAcquire(key, holderId, now, staleMs) {
|
|
84
|
+
await ensureBuildLockSchema(client);
|
|
85
|
+
const result = await client.execute({
|
|
86
|
+
sql: `INSERT INTO chatter_build_lock (lock_key, holder_id, heartbeat_at) VALUES (?, ?, ?)
|
|
87
|
+
ON CONFLICT(lock_key) DO UPDATE SET
|
|
88
|
+
holder_id = excluded.holder_id,
|
|
89
|
+
heartbeat_at = excluded.heartbeat_at
|
|
90
|
+
WHERE chatter_build_lock.holder_id = excluded.holder_id
|
|
91
|
+
OR chatter_build_lock.heartbeat_at <= excluded.heartbeat_at - ?
|
|
92
|
+
RETURNING holder_id`,
|
|
93
|
+
args: [key, holderId, now, staleMs]
|
|
94
|
+
});
|
|
95
|
+
const row = result.rows[0];
|
|
96
|
+
return Boolean(row) && row.holder_id === holderId;
|
|
97
|
+
},
|
|
98
|
+
async release(key, holderId) {
|
|
99
|
+
await ensureBuildLockSchema(client);
|
|
100
|
+
await client.execute({
|
|
101
|
+
sql: "DELETE FROM chatter_build_lock WHERE lock_key = ? AND holder_id = ?",
|
|
102
|
+
args: [key, holderId]
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
var BUILD_LOCK_KEY, BUILD_LOCK_STALE_MS, schemaReady;
|
|
108
|
+
var init_buildLock = __esm({
|
|
109
|
+
"src/core/buildLock.ts"() {
|
|
110
|
+
"use strict";
|
|
111
|
+
BUILD_LOCK_KEY = "knowledge_build";
|
|
112
|
+
BUILD_LOCK_STALE_MS = 10 * 6e4;
|
|
113
|
+
schemaReady = /* @__PURE__ */ new WeakMap();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/core/loaders.ts
|
|
118
|
+
function walk(dir) {
|
|
119
|
+
const items = [];
|
|
120
|
+
try {
|
|
121
|
+
for (const e of (0, import_node_fs2.readdirSync)(dir)) {
|
|
122
|
+
const p = (0, import_node_path2.join)(dir, e);
|
|
123
|
+
const st = (0, import_node_fs2.statSync)(p);
|
|
124
|
+
if (st.isDirectory()) items.push(...walk(p));
|
|
125
|
+
else if (p.endsWith(".md")) items.push(p);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
return items;
|
|
130
|
+
}
|
|
131
|
+
function loadKnowledge(root = "config/knowledge") {
|
|
132
|
+
const docs = [];
|
|
133
|
+
for (const bucket of ["base", "public", "private"]) {
|
|
134
|
+
const dir = (0, import_node_path2.join)(root, bucket);
|
|
135
|
+
for (const f of walk(dir)) {
|
|
136
|
+
docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return docs;
|
|
140
|
+
}
|
|
141
|
+
var import_node_fs2, import_node_path2;
|
|
142
|
+
var init_loaders = __esm({
|
|
143
|
+
"src/core/loaders.ts"() {
|
|
144
|
+
"use strict";
|
|
145
|
+
import_node_fs2 = require("node:fs");
|
|
146
|
+
import_node_path2 = require("node:path");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// src/core/retrieval.ts
|
|
151
|
+
var retrieval_exports = {};
|
|
152
|
+
__export(retrieval_exports, {
|
|
153
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE: () => DATABASE_CONFIG_REQUIRED_MESSAGE,
|
|
154
|
+
DEFAULT_KNOWLEDGE_DIR: () => DEFAULT_KNOWLEDGE_DIR,
|
|
155
|
+
VectorStore: () => VectorStore,
|
|
156
|
+
createOpenAIEmbedder: () => createOpenAIEmbedder,
|
|
157
|
+
openLibsqlClient: () => openLibsqlClient,
|
|
158
|
+
wrapMissingLibsqlError: () => wrapMissingLibsqlError
|
|
159
|
+
});
|
|
160
|
+
function createOpenAIEmbedder(client) {
|
|
161
|
+
return async (input) => {
|
|
162
|
+
const res = await client.embeddings.create({ model: EMB_MODEL, input });
|
|
163
|
+
return res.data.map((d) => d.embedding);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function wrapMissingLibsqlError(cause) {
|
|
167
|
+
return new Error(
|
|
168
|
+
"Chatter's default knowledge store needs the optional peer dependency '@libsql/client', which is not installed. Install it with `bun add @libsql/client` (or npm/pnpm/yarn), or set config.retriever to use your own retrieval backend instead.",
|
|
169
|
+
{ cause }
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
async function openLibsqlClient(database) {
|
|
173
|
+
const mod = await import("@libsql/client").catch((error) => {
|
|
174
|
+
throw wrapMissingLibsqlError(error);
|
|
175
|
+
});
|
|
176
|
+
return mod.createClient({ url: database.url, authToken: database.authToken });
|
|
177
|
+
}
|
|
178
|
+
function chunk(text, max = 900) {
|
|
179
|
+
const out = [];
|
|
180
|
+
let buf = "";
|
|
181
|
+
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
182
|
+
if (`${buf}
|
|
183
|
+
${line}`.length > max) {
|
|
184
|
+
out.push(buf.trim());
|
|
185
|
+
buf = line;
|
|
186
|
+
} else buf += `
|
|
187
|
+
${line}`;
|
|
188
|
+
}
|
|
189
|
+
if (buf.trim()) out.push(buf.trim());
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
async function sha256(input) {
|
|
193
|
+
const data = new TextEncoder().encode(input);
|
|
194
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
195
|
+
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
196
|
+
}
|
|
197
|
+
var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
|
|
198
|
+
var init_retrieval = __esm({
|
|
199
|
+
"src/core/retrieval.ts"() {
|
|
200
|
+
"use strict";
|
|
201
|
+
init_buildLock();
|
|
202
|
+
init_loaders();
|
|
203
|
+
init_logger();
|
|
204
|
+
EMB_MODEL = "text-embedding-3-large";
|
|
205
|
+
DEFAULT_KNOWLEDGE_DIR = "./config/knowledge";
|
|
206
|
+
DATABASE_CONFIG_REQUIRED_MESSAGE = "config.database is required unless config.retriever is set - Chatter's default knowledge store (VectorStore) needs a Turso/libsql connection. Set config.database, or supply config.retriever to use your own retrieval backend instead.";
|
|
207
|
+
VectorStore = class _VectorStore {
|
|
208
|
+
constructor(embed, options) {
|
|
209
|
+
this.embed = embed;
|
|
210
|
+
this.db = options.databaseClient;
|
|
211
|
+
this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
|
|
212
|
+
this.logger = options.logger ?? createConsoleLogger();
|
|
213
|
+
this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
|
|
214
|
+
this.instanceId = options.instanceId ?? crypto.randomUUID();
|
|
215
|
+
this.buildLockStaleMs = options.buildLockStaleMs ?? BUILD_LOCK_STALE_MS;
|
|
216
|
+
}
|
|
217
|
+
embed;
|
|
218
|
+
/**
|
|
219
|
+
* The libsql client backing this store - the same instance passed in as
|
|
220
|
+
* `databaseClient`, so callers holding it (e.g. `ServerDependencies.db`)
|
|
221
|
+
* and the store share one connection.
|
|
222
|
+
*/
|
|
223
|
+
db;
|
|
224
|
+
knowledgeDir;
|
|
225
|
+
logger;
|
|
226
|
+
buildLock;
|
|
227
|
+
instanceId;
|
|
228
|
+
buildLockStaleMs;
|
|
229
|
+
/**
|
|
230
|
+
* On boot: ingest new chunks and embed only missing ones.
|
|
231
|
+
*
|
|
232
|
+
* The whole ingest runs under a single-writer lock held in the database
|
|
233
|
+
* (see [buildLock.ts](./buildLock.ts)), because the cleanup step deletes
|
|
234
|
+
* every chunk id the current `knowledgeDir` did not produce. Without the
|
|
235
|
+
* lock, a second instance booting mid-build diffs against a database the
|
|
236
|
+
* first one is still writing and deletes its chunks. An instance that
|
|
237
|
+
* cannot take the lock skips its build entirely and serves what the holder
|
|
238
|
+
* has already written, rather than racing it.
|
|
239
|
+
*/
|
|
240
|
+
async build() {
|
|
241
|
+
this.logger.info("Building knowledge base...");
|
|
242
|
+
await this.db.execute(`
|
|
243
|
+
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
244
|
+
`);
|
|
245
|
+
await this.db.execute(`
|
|
246
|
+
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
247
|
+
`);
|
|
248
|
+
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
249
|
+
if (!await this.renewBuildLock()) {
|
|
250
|
+
this.logger.warn(
|
|
251
|
+
"Another instance is building the knowledge base against this database - skipping this build and using what it has written. Nothing was deleted."
|
|
252
|
+
);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
await this.buildUnderLock();
|
|
257
|
+
} finally {
|
|
258
|
+
await this.buildLock.release(BUILD_LOCK_KEY, this.instanceId);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Acquires the build lock, or refreshes the heartbeat if this store already holds it. */
|
|
262
|
+
renewBuildLock() {
|
|
263
|
+
return this.buildLock.tryAcquire(
|
|
264
|
+
BUILD_LOCK_KEY,
|
|
265
|
+
this.instanceId,
|
|
266
|
+
Date.now(),
|
|
267
|
+
this.buildLockStaleMs
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
/** The ingest itself. Only ever runs with the build lock held. */
|
|
271
|
+
async buildUnderLock() {
|
|
272
|
+
const docs = loadKnowledge(this.knowledgeDir);
|
|
273
|
+
this.logger.info(`Loaded ${docs.length} knowledge documents`);
|
|
274
|
+
if (docs.length === 0) {
|
|
275
|
+
const existing = await this.db.execute("SELECT COUNT(*) as count FROM chunks");
|
|
276
|
+
const existingCount = Number(existing.rows[0]?.count ?? 0);
|
|
277
|
+
if (existingCount > 0) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
`Refusing to build: loaded 0 knowledge documents from "${this.knowledgeDir}", but ${existingCount} chunks already exist. Proceeding would delete all of them as stale. Check that knowledgeDir resolves to the right path.`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
this.logger.info("No knowledge documents and no existing chunks - nothing to build");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const rows = [];
|
|
286
|
+
for (const d of docs) {
|
|
287
|
+
for (const part of chunk(d.text)) {
|
|
288
|
+
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
289
|
+
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
|
|
293
|
+
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
294
|
+
const UPSERT_BATCH = 500;
|
|
295
|
+
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
296
|
+
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
297
|
+
await this.db.batch(
|
|
298
|
+
batch.map((r) => ({
|
|
299
|
+
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
300
|
+
ON CONFLICT(id) DO NOTHING`,
|
|
301
|
+
args: [r.id, r.bucket, r.source, r.text]
|
|
302
|
+
})),
|
|
303
|
+
"write"
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const ids = rows.map((r) => r.id);
|
|
307
|
+
const missing = [];
|
|
308
|
+
for (let i = 0; i < ids.length; i += 500) {
|
|
309
|
+
const batch = ids.slice(i, i + 500);
|
|
310
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
311
|
+
const res = await this.db.execute({
|
|
312
|
+
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
313
|
+
EXCEPT SELECT id FROM embeddings`,
|
|
314
|
+
args: batch
|
|
315
|
+
});
|
|
316
|
+
for (const row of res.rows) missing.push(String(row.id));
|
|
317
|
+
}
|
|
318
|
+
if (missing.length === 0) {
|
|
319
|
+
this.logger.info("No new chunks to embed - knowledge base is up to date");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
323
|
+
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
324
|
+
const BATCH = 96;
|
|
325
|
+
for (let i = 0; i < missing.length; i += BATCH) {
|
|
326
|
+
const batchIds = missing.slice(i, i + BATCH);
|
|
327
|
+
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
328
|
+
const vectors = await this.embed(inputs);
|
|
329
|
+
await this.db.batch(
|
|
330
|
+
vectors.map((embedding, idx) => ({
|
|
331
|
+
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
332
|
+
args: [batchIds[idx], EMB_MODEL, JSON.stringify(embedding)]
|
|
333
|
+
})),
|
|
334
|
+
"write"
|
|
335
|
+
);
|
|
336
|
+
if (!await this.renewBuildLock()) {
|
|
337
|
+
this.logger.warn(
|
|
338
|
+
"Lost the knowledge-base build lock to another instance mid-build - remaining chunks may be embedded by that instance instead."
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this.logger.info(`Successfully embedded ${missing.length} new chunks`);
|
|
343
|
+
}
|
|
344
|
+
// Remove chunks from database that no longer exist in markdown files
|
|
345
|
+
async cleanupStaleChunks(currentIds) {
|
|
346
|
+
const result = await this.db.execute("SELECT id FROM chunks");
|
|
347
|
+
const dbIds = result.rows.map((row) => String(row.id));
|
|
348
|
+
const currentIdSet = new Set(currentIds);
|
|
349
|
+
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
350
|
+
if (staleIds.length === 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
this.logger.info(`Cleaning up ${staleIds.length} stale chunks...`);
|
|
354
|
+
const BATCH_SIZE = 500;
|
|
355
|
+
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
356
|
+
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
357
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
358
|
+
await this.db.batch(
|
|
359
|
+
[
|
|
360
|
+
{ sql: `DELETE FROM chunks WHERE id IN (${placeholders})`, args: batch },
|
|
361
|
+
{ sql: `DELETE FROM embeddings WHERE id IN (${placeholders})`, args: batch }
|
|
362
|
+
],
|
|
363
|
+
"write"
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
this.logger.info(`Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
367
|
+
}
|
|
368
|
+
static cosine(a, b) {
|
|
369
|
+
let dot = 0;
|
|
370
|
+
let na = 0;
|
|
371
|
+
let nb = 0;
|
|
372
|
+
for (let i = 0; i < a.length; i++) {
|
|
373
|
+
dot += a[i] * b[i];
|
|
374
|
+
na += a[i] * a[i];
|
|
375
|
+
nb += b[i] * b[i];
|
|
376
|
+
}
|
|
377
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
381
|
+
* & portable).
|
|
382
|
+
*
|
|
383
|
+
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
384
|
+
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
385
|
+
* buckets can query them (`build` only writes and prunes the three the
|
|
386
|
+
* knowledge loader walks). Names are bound as query parameters, never
|
|
387
|
+
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
388
|
+
* the embedding call.
|
|
389
|
+
*/
|
|
390
|
+
async query(q, k = 6, allowed = ["base"]) {
|
|
391
|
+
if (allowed.length === 0) return [];
|
|
392
|
+
const [qv] = await this.embed([q]);
|
|
393
|
+
const placeholders = allowed.map(() => "?").join(",");
|
|
394
|
+
const res = await this.db.execute({
|
|
395
|
+
sql: `SELECT c.id, c.text, e.embedding
|
|
396
|
+
FROM chunks c
|
|
397
|
+
JOIN embeddings e ON e.id = c.id
|
|
398
|
+
WHERE c.bucket IN (${placeholders})`,
|
|
399
|
+
args: allowed
|
|
400
|
+
});
|
|
401
|
+
const scored = [];
|
|
402
|
+
for (const row of res.rows) {
|
|
403
|
+
const emb = JSON.parse(String(row.embedding));
|
|
404
|
+
const s = _VectorStore.cosine(qv, emb);
|
|
405
|
+
scored.push({ s, text: String(row.text) });
|
|
406
|
+
}
|
|
407
|
+
scored.sort((a, b) => b.s - a.s);
|
|
408
|
+
return scored.slice(0, k).map((r) => r.text);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
30
414
|
// src/server.ts
|
|
31
415
|
var server_exports = {};
|
|
32
416
|
__export(server_exports, {
|
|
@@ -34,7 +418,6 @@ __export(server_exports, {
|
|
|
34
418
|
});
|
|
35
419
|
module.exports = __toCommonJS(server_exports);
|
|
36
420
|
var import_node_path4 = require("node:path");
|
|
37
|
-
var import_client2 = require("@libsql/client");
|
|
38
421
|
var import_hono5 = require("hono");
|
|
39
422
|
var import_openai = __toESM(require("openai"));
|
|
40
423
|
|
|
@@ -252,7 +635,8 @@ async function answerOnce({
|
|
|
252
635
|
async function applyTransformReply(transformReply, input, logger) {
|
|
253
636
|
if (!transformReply) return input.text;
|
|
254
637
|
try {
|
|
255
|
-
|
|
638
|
+
const result = await transformReply(input);
|
|
639
|
+
return typeof result === "string" ? result : null;
|
|
256
640
|
} catch (error) {
|
|
257
641
|
logger?.error(
|
|
258
642
|
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
@@ -382,6 +766,7 @@ async function prepareChat({
|
|
|
382
766
|
sender,
|
|
383
767
|
rewriteQuery,
|
|
384
768
|
rerankContext,
|
|
769
|
+
fallbackFn,
|
|
385
770
|
logger
|
|
386
771
|
}) {
|
|
387
772
|
const lastUserMsg = lastUserMessage(messages);
|
|
@@ -398,47 +783,47 @@ async function prepareChat({
|
|
|
398
783
|
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
399
784
|
}
|
|
400
785
|
}
|
|
401
|
-
|
|
786
|
+
const resolvedBuckets = buckets ?? defaultBuckets(mode);
|
|
787
|
+
const retrievedChunks = await store.query(query, topK, resolvedBuckets);
|
|
788
|
+
let ctx = retrievedChunks;
|
|
402
789
|
if (rerankContext) {
|
|
403
790
|
try {
|
|
404
|
-
const reranked = await rerankContext({ query, chunks: ctx });
|
|
791
|
+
const reranked = await rerankContext({ query, chunks: [...ctx] });
|
|
405
792
|
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
406
793
|
} catch (error) {
|
|
407
794
|
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
408
795
|
}
|
|
409
796
|
}
|
|
797
|
+
let fallback;
|
|
798
|
+
if (ctx.length === 0 && fallbackFn) {
|
|
799
|
+
try {
|
|
800
|
+
const guidance = await fallbackFn({
|
|
801
|
+
query,
|
|
802
|
+
mode,
|
|
803
|
+
buckets: resolvedBuckets,
|
|
804
|
+
sender,
|
|
805
|
+
retrievedChunks
|
|
806
|
+
});
|
|
807
|
+
if (typeof guidance === "string" && guidance.trim()) fallback = guidance.trim();
|
|
808
|
+
} catch (error) {
|
|
809
|
+
logger?.error("fallbackFn threw; proceeding without fallback guidance", error);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
410
812
|
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
411
813
|
const hint = channelHint?.trim();
|
|
412
814
|
const system = [
|
|
413
815
|
prompts.baseSystemRules,
|
|
414
816
|
persona,
|
|
415
817
|
...hint ? [hint] : [],
|
|
818
|
+
...fallback ? [fallback] : [],
|
|
416
819
|
`${label}:
|
|
417
820
|
${ctx.join("\n\n")}`
|
|
418
821
|
].join("\n\n");
|
|
419
822
|
return { system, messages, context: ctx };
|
|
420
823
|
}
|
|
421
824
|
|
|
422
|
-
// src/core/logger.ts
|
|
423
|
-
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
424
|
-
function createConsoleLogger(level = "info") {
|
|
425
|
-
const threshold = LEVEL_ORDER[level];
|
|
426
|
-
const write = (at) => (...args) => {
|
|
427
|
-
if (LEVEL_ORDER[at] < threshold) return;
|
|
428
|
-
console.error(`[${at}]`, ...args);
|
|
429
|
-
};
|
|
430
|
-
return {
|
|
431
|
-
debug: write("debug"),
|
|
432
|
-
info: write("info"),
|
|
433
|
-
warn: write("warn"),
|
|
434
|
-
error: write("error")
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
function resolveLogger(logger, level) {
|
|
438
|
-
return logger ?? createConsoleLogger(level);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
825
|
// src/channels/senders.ts
|
|
826
|
+
init_logger();
|
|
442
827
|
function createSenderRegistry(logger = createConsoleLogger()) {
|
|
443
828
|
const senders = /* @__PURE__ */ new Map();
|
|
444
829
|
return {
|
|
@@ -501,9 +886,13 @@ function createSenderRegistry(logger = createConsoleLogger()) {
|
|
|
501
886
|
};
|
|
502
887
|
}
|
|
503
888
|
|
|
889
|
+
// src/server.ts
|
|
890
|
+
init_logger();
|
|
891
|
+
|
|
504
892
|
// src/core/prompts.ts
|
|
505
893
|
var import_node_fs = require("node:fs");
|
|
506
894
|
var import_node_path = require("node:path");
|
|
895
|
+
var DEFAULT_PROMPTS_DIR = "./config/prompts";
|
|
507
896
|
var PromptLoader = class {
|
|
508
897
|
constructor(promptsDir, bot) {
|
|
509
898
|
this.promptsDir = promptsDir;
|
|
@@ -534,206 +923,8 @@ var PromptLoader = class {
|
|
|
534
923
|
}
|
|
535
924
|
};
|
|
536
925
|
|
|
537
|
-
// src/
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
// src/core/loaders.ts
|
|
541
|
-
var import_node_fs2 = require("node:fs");
|
|
542
|
-
var import_node_path2 = require("node:path");
|
|
543
|
-
function walk(dir) {
|
|
544
|
-
const items = [];
|
|
545
|
-
try {
|
|
546
|
-
for (const e of (0, import_node_fs2.readdirSync)(dir)) {
|
|
547
|
-
const p = (0, import_node_path2.join)(dir, e);
|
|
548
|
-
const st = (0, import_node_fs2.statSync)(p);
|
|
549
|
-
if (st.isDirectory()) items.push(...walk(p));
|
|
550
|
-
else if (p.endsWith(".md")) items.push(p);
|
|
551
|
-
}
|
|
552
|
-
} catch {
|
|
553
|
-
}
|
|
554
|
-
return items;
|
|
555
|
-
}
|
|
556
|
-
function loadKnowledge(root = "config/knowledge") {
|
|
557
|
-
const docs = [];
|
|
558
|
-
for (const bucket of ["base", "public", "private"]) {
|
|
559
|
-
const dir = (0, import_node_path2.join)(root, bucket);
|
|
560
|
-
for (const f of walk(dir)) {
|
|
561
|
-
docs.push({ id: f, text: (0, import_node_fs2.readFileSync)(f, "utf8"), source: f, bucket });
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
return docs;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// src/core/retrieval.ts
|
|
568
|
-
var EMB_MODEL = "text-embedding-3-large";
|
|
569
|
-
function chunk(text, max = 900) {
|
|
570
|
-
const out = [];
|
|
571
|
-
let buf = "";
|
|
572
|
-
for (const line of text.replace(/\r/g, "").split("\n")) {
|
|
573
|
-
if (`${buf}
|
|
574
|
-
${line}`.length > max) {
|
|
575
|
-
out.push(buf.trim());
|
|
576
|
-
buf = line;
|
|
577
|
-
} else buf += `
|
|
578
|
-
${line}`;
|
|
579
|
-
}
|
|
580
|
-
if (buf.trim()) out.push(buf.trim());
|
|
581
|
-
return out;
|
|
582
|
-
}
|
|
583
|
-
async function sha256(input) {
|
|
584
|
-
const data = new TextEncoder().encode(input);
|
|
585
|
-
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
586
|
-
return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
587
|
-
}
|
|
588
|
-
var VectorStore = class _VectorStore {
|
|
589
|
-
constructor(client, options) {
|
|
590
|
-
this.client = client;
|
|
591
|
-
this.db = options.databaseClient ? options.databaseClient : (0, import_client.createClient)({
|
|
592
|
-
url: options.databaseUrl,
|
|
593
|
-
authToken: options.databaseAuthToken
|
|
594
|
-
});
|
|
595
|
-
this.knowledgeDir = options.knowledgeDir || "./config/knowledge";
|
|
596
|
-
this.logger = options.logger ?? createConsoleLogger();
|
|
597
|
-
}
|
|
598
|
-
client;
|
|
599
|
-
/**
|
|
600
|
-
* The libsql client backing this store. When a client was injected this is
|
|
601
|
-
* that same instance, so callers holding it (e.g. `ServerDependencies.db`)
|
|
602
|
-
* and the store share one connection.
|
|
603
|
-
*/
|
|
604
|
-
db;
|
|
605
|
-
knowledgeDir;
|
|
606
|
-
logger;
|
|
607
|
-
// On boot: ingest new chunks and embed only missing ones.
|
|
608
|
-
async build() {
|
|
609
|
-
this.logger.info("\u{1F504} Building knowledge base...");
|
|
610
|
-
await this.db.execute(`
|
|
611
|
-
CREATE TABLE IF NOT EXISTS chunks (id TEXT PRIMARY KEY, bucket TEXT NOT NULL, source TEXT NOT NULL, text TEXT NOT NULL);
|
|
612
|
-
`);
|
|
613
|
-
await this.db.execute(`
|
|
614
|
-
CREATE TABLE IF NOT EXISTS embeddings (id TEXT PRIMARY KEY, model TEXT NOT NULL, embedding BLOB NOT NULL);
|
|
615
|
-
`);
|
|
616
|
-
await this.db.execute("CREATE INDEX IF NOT EXISTS idx_chunks_bucket ON chunks(bucket);");
|
|
617
|
-
const docs = loadKnowledge(this.knowledgeDir);
|
|
618
|
-
this.logger.info(`\u{1F4DA} Loaded ${docs.length} knowledge documents`);
|
|
619
|
-
const rows = [];
|
|
620
|
-
for (const d of docs) {
|
|
621
|
-
for (const part of chunk(d.text)) {
|
|
622
|
-
const id = await sha256(`${d.bucket}|${d.source}|${part}`);
|
|
623
|
-
rows.push({ id, bucket: d.bucket, source: d.source, text: part });
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
this.logger.info(`\u{1F4E6} Created ${rows.length} chunks from knowledge documents`);
|
|
627
|
-
await this.cleanupStaleChunks(rows.map((r) => r.id));
|
|
628
|
-
const UPSERT_BATCH = 500;
|
|
629
|
-
for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
|
|
630
|
-
const batch = rows.slice(i, i + UPSERT_BATCH);
|
|
631
|
-
await this.db.batch(
|
|
632
|
-
batch.map((r) => ({
|
|
633
|
-
sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
|
|
634
|
-
ON CONFLICT(id) DO NOTHING`,
|
|
635
|
-
args: [r.id, r.bucket, r.source, r.text]
|
|
636
|
-
})),
|
|
637
|
-
"write"
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
const ids = rows.map((r) => r.id);
|
|
641
|
-
const missing = [];
|
|
642
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
643
|
-
const batch = ids.slice(i, i + 500);
|
|
644
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
645
|
-
const res = await this.db.execute({
|
|
646
|
-
sql: `SELECT id FROM chunks WHERE id IN (${placeholders})
|
|
647
|
-
EXCEPT SELECT id FROM embeddings`,
|
|
648
|
-
args: batch
|
|
649
|
-
});
|
|
650
|
-
for (const row of res.rows) missing.push(String(row.id));
|
|
651
|
-
}
|
|
652
|
-
if (missing.length === 0) {
|
|
653
|
-
this.logger.info("\u2705 No new chunks to embed - knowledge base is up to date");
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
this.logger.info(`\u{1F52E} Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
|
|
657
|
-
const textById = new Map(rows.map((r) => [r.id, r.text]));
|
|
658
|
-
const BATCH = 96;
|
|
659
|
-
for (let i = 0; i < missing.length; i += BATCH) {
|
|
660
|
-
const batchIds = missing.slice(i, i + BATCH);
|
|
661
|
-
const inputs = batchIds.map((id) => textById.get(id) || "");
|
|
662
|
-
const emb = await this.client.embeddings.create({ model: EMB_MODEL, input: inputs });
|
|
663
|
-
await this.db.batch(
|
|
664
|
-
emb.data.map((d, idx) => ({
|
|
665
|
-
sql: "INSERT INTO embeddings(id,model,embedding) VALUES(?,?,?)",
|
|
666
|
-
args: [batchIds[idx], EMB_MODEL, JSON.stringify(d.embedding)]
|
|
667
|
-
})),
|
|
668
|
-
"write"
|
|
669
|
-
);
|
|
670
|
-
}
|
|
671
|
-
this.logger.info(`\u2705 Successfully embedded ${missing.length} new chunks`);
|
|
672
|
-
}
|
|
673
|
-
// Remove chunks from database that no longer exist in markdown files
|
|
674
|
-
async cleanupStaleChunks(currentIds) {
|
|
675
|
-
const result = await this.db.execute("SELECT id FROM chunks");
|
|
676
|
-
const dbIds = result.rows.map((row) => String(row.id));
|
|
677
|
-
const currentIdSet = new Set(currentIds);
|
|
678
|
-
const staleIds = dbIds.filter((id) => !currentIdSet.has(id));
|
|
679
|
-
if (staleIds.length === 0) {
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
this.logger.info(`\u{1F9F9} Cleaning up ${staleIds.length} stale chunks...`);
|
|
683
|
-
const BATCH_SIZE = 500;
|
|
684
|
-
for (let i = 0; i < staleIds.length; i += BATCH_SIZE) {
|
|
685
|
-
const batch = staleIds.slice(i, i + BATCH_SIZE);
|
|
686
|
-
const placeholders = batch.map(() => "?").join(",");
|
|
687
|
-
await this.db.execute({
|
|
688
|
-
sql: `DELETE FROM chunks WHERE id IN (${placeholders})`,
|
|
689
|
-
args: batch
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
this.logger.info(`\u2713 Cleaned up ${staleIds.length} stale chunks and their embeddings`);
|
|
693
|
-
}
|
|
694
|
-
static cosine(a, b) {
|
|
695
|
-
let dot = 0;
|
|
696
|
-
let na = 0;
|
|
697
|
-
let nb = 0;
|
|
698
|
-
for (let i = 0; i < a.length; i++) {
|
|
699
|
-
dot += a[i] * b[i];
|
|
700
|
-
na += a[i] * a[i];
|
|
701
|
-
nb += b[i] * b[i];
|
|
702
|
-
}
|
|
703
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Retrieve top-k across allowed buckets; compute similarity in app (simple
|
|
707
|
-
* & portable).
|
|
708
|
-
*
|
|
709
|
-
* `allowed` is a bucket-name list rather than the {@link Bucket} union: the
|
|
710
|
-
* `chunks` table constrains nothing, so a deployment that ingests its own
|
|
711
|
-
* buckets can query them (`build` only writes and prunes the three the
|
|
712
|
-
* knowledge loader walks). Names are bound as query parameters, never
|
|
713
|
-
* interpolated. An empty list retrieves nothing, and short-circuits before
|
|
714
|
-
* the embedding call.
|
|
715
|
-
*/
|
|
716
|
-
async query(q, k = 6, allowed = ["base"]) {
|
|
717
|
-
if (allowed.length === 0) return [];
|
|
718
|
-
const qv = (await this.client.embeddings.create({ model: EMB_MODEL, input: [q] })).data[0].embedding;
|
|
719
|
-
const placeholders = allowed.map(() => "?").join(",");
|
|
720
|
-
const res = await this.db.execute({
|
|
721
|
-
sql: `SELECT c.id, c.text, e.embedding
|
|
722
|
-
FROM chunks c
|
|
723
|
-
JOIN embeddings e ON e.id = c.id
|
|
724
|
-
WHERE c.bucket IN (${placeholders})`,
|
|
725
|
-
args: allowed
|
|
726
|
-
});
|
|
727
|
-
const scored = [];
|
|
728
|
-
for (const row of res.rows) {
|
|
729
|
-
const emb = JSON.parse(String(row.embedding));
|
|
730
|
-
const s = _VectorStore.cosine(qv, emb);
|
|
731
|
-
scored.push({ s, text: String(row.text) });
|
|
732
|
-
}
|
|
733
|
-
scored.sort((a, b) => b.s - a.s);
|
|
734
|
-
return scored.slice(0, k).map((r) => r.text);
|
|
735
|
-
}
|
|
736
|
-
};
|
|
926
|
+
// src/server.ts
|
|
927
|
+
init_retrieval();
|
|
737
928
|
|
|
738
929
|
// src/core/serve-static.ts
|
|
739
930
|
var STATIC_ADAPTERS = {
|
|
@@ -792,8 +983,12 @@ function resolveStatic(configStaticDir) {
|
|
|
792
983
|
return { staticDir: null };
|
|
793
984
|
}
|
|
794
985
|
|
|
986
|
+
// src/middleware/apiKey.ts
|
|
987
|
+
var API_KEY_HEADER = "x-api-key";
|
|
988
|
+
var API_KEY_PREVIEW_LENGTH = 20;
|
|
989
|
+
|
|
795
990
|
// src/middleware/cors.ts
|
|
796
|
-
var ALLOWED_HEADERS =
|
|
991
|
+
var ALLOWED_HEADERS = `content-type, ${API_KEY_HEADER}, authorization, x-conversation-id`;
|
|
797
992
|
var ALLOWED_METHODS = "POST, GET, OPTIONS";
|
|
798
993
|
var EXPOSED_HEADERS = "x-conversation-id";
|
|
799
994
|
var cors = (allowedOrigins) => async (c, next) => {
|
|
@@ -857,22 +1052,27 @@ var import_streaming = require("hono/streaming");
|
|
|
857
1052
|
|
|
858
1053
|
// src/core/session.ts
|
|
859
1054
|
var import_node_crypto2 = require("node:crypto");
|
|
1055
|
+
var SESSION_KEY_PREFIX = "session_";
|
|
1056
|
+
var DEFAULT_SESSION_MAX_REQUESTS = 20;
|
|
1057
|
+
var DEFAULT_SESSION_TTL_SECONDS = 3600;
|
|
1058
|
+
var SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
860
1059
|
var sessions = /* @__PURE__ */ new Map();
|
|
861
|
-
var sessionSweeper = setInterval(
|
|
862
|
-
()
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
sessions.delete(key);
|
|
867
|
-
}
|
|
1060
|
+
var sessionSweeper = setInterval(() => {
|
|
1061
|
+
const now = Date.now();
|
|
1062
|
+
for (const [key, data] of Array.from(sessions.entries())) {
|
|
1063
|
+
if (now > data.expiresAt) {
|
|
1064
|
+
sessions.delete(key);
|
|
868
1065
|
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
);
|
|
1066
|
+
}
|
|
1067
|
+
}, SESSION_SWEEP_INTERVAL_MS);
|
|
872
1068
|
sessionSweeper.unref?.();
|
|
873
1069
|
function createSession(options = {}) {
|
|
874
|
-
const {
|
|
875
|
-
|
|
1070
|
+
const {
|
|
1071
|
+
maxRequests = DEFAULT_SESSION_MAX_REQUESTS,
|
|
1072
|
+
ttl = DEFAULT_SESSION_TTL_SECONDS,
|
|
1073
|
+
metadata
|
|
1074
|
+
} = options;
|
|
1075
|
+
const key = `${SESSION_KEY_PREFIX}${(0, import_node_crypto2.randomUUID)()}`;
|
|
876
1076
|
const now = Date.now();
|
|
877
1077
|
const session = {
|
|
878
1078
|
key,
|
|
@@ -1035,6 +1235,8 @@ function limitDemo(trustProxy) {
|
|
|
1035
1235
|
await next();
|
|
1036
1236
|
};
|
|
1037
1237
|
}
|
|
1238
|
+
var DEMO_SESSION_MAX_REQUESTS = 20;
|
|
1239
|
+
var DEMO_SESSION_TTL_SECONDS = 3600;
|
|
1038
1240
|
function demoRoutes(deps) {
|
|
1039
1241
|
const { client, store, config, prompts, logger } = deps;
|
|
1040
1242
|
const app = new import_hono.Hono();
|
|
@@ -1047,18 +1249,17 @@ function demoRoutes(deps) {
|
|
|
1047
1249
|
limitSessionCreation(trustProxy),
|
|
1048
1250
|
(c) => {
|
|
1049
1251
|
const session = createSession({
|
|
1050
|
-
maxRequests:
|
|
1051
|
-
ttl:
|
|
1052
|
-
// 1 hour
|
|
1252
|
+
maxRequests: DEMO_SESSION_MAX_REQUESTS,
|
|
1253
|
+
ttl: DEMO_SESSION_TTL_SECONDS
|
|
1053
1254
|
});
|
|
1054
1255
|
logger.debug(
|
|
1055
|
-
`[Demo] Created session: ${session.key.slice(0,
|
|
1256
|
+
`[Demo] Created session: ${session.key.slice(0, API_KEY_PREVIEW_LENGTH)}... (${session.maxRequests} requests, ${DEMO_SESSION_TTL_SECONDS}s TTL)`
|
|
1056
1257
|
);
|
|
1057
1258
|
return c.json({
|
|
1058
1259
|
key: session.key,
|
|
1059
|
-
expiresIn:
|
|
1260
|
+
expiresIn: DEMO_SESSION_TTL_SECONDS,
|
|
1060
1261
|
maxRequests: session.maxRequests,
|
|
1061
|
-
message:
|
|
1262
|
+
message: `Use this temporary key in the ${API_KEY_HEADER} header`
|
|
1062
1263
|
});
|
|
1063
1264
|
}
|
|
1064
1265
|
);
|
|
@@ -1092,6 +1293,7 @@ function demoRoutes(deps) {
|
|
|
1092
1293
|
buckets,
|
|
1093
1294
|
rewriteQuery: config.rewriteQuery,
|
|
1094
1295
|
rerankContext: config.rerankContext,
|
|
1296
|
+
fallbackFn: config.fallbackFn,
|
|
1095
1297
|
logger
|
|
1096
1298
|
});
|
|
1097
1299
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
@@ -1194,7 +1396,7 @@ function createRateLimiter(config) {
|
|
|
1194
1396
|
// Demo keys get 10x stricter rate limits
|
|
1195
1397
|
limitPublic() {
|
|
1196
1398
|
return async (c, next) => {
|
|
1197
|
-
const apiKey = c.req.header(
|
|
1399
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1198
1400
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1199
1401
|
const key = `pub:${clientRateLimitKey(c, trustProxy)}`;
|
|
1200
1402
|
const limit = isDemoKey ? Math.floor(publicLimit / 10) : publicLimit;
|
|
@@ -1263,9 +1465,13 @@ function openaiRoutes(deps) {
|
|
|
1263
1465
|
return errorJson(c, 401, "API key authentication is not configured on this server");
|
|
1264
1466
|
}
|
|
1265
1467
|
const bearer = c.req.header("authorization") ?? "";
|
|
1266
|
-
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(
|
|
1468
|
+
const key = bearer.startsWith("Bearer ") ? bearer.slice("Bearer ".length) : c.req.header(API_KEY_HEADER);
|
|
1267
1469
|
if (!key) {
|
|
1268
|
-
return errorJson(
|
|
1470
|
+
return errorJson(
|
|
1471
|
+
c,
|
|
1472
|
+
401,
|
|
1473
|
+
`Missing API key (Authorization: Bearer or ${API_KEY_HEADER} header)`
|
|
1474
|
+
);
|
|
1269
1475
|
}
|
|
1270
1476
|
try {
|
|
1271
1477
|
const result = await apiKeyManager.verify(key);
|
|
@@ -1305,6 +1511,7 @@ function openaiRoutes(deps) {
|
|
|
1305
1511
|
sender,
|
|
1306
1512
|
rewriteQuery: config.rewriteQuery,
|
|
1307
1513
|
rerankContext: config.rerankContext,
|
|
1514
|
+
fallbackFn: config.fallbackFn,
|
|
1308
1515
|
logger
|
|
1309
1516
|
}));
|
|
1310
1517
|
} catch {
|
|
@@ -1428,6 +1635,7 @@ function privateRoutes(deps) {
|
|
|
1428
1635
|
sender,
|
|
1429
1636
|
rewriteQuery: config.rewriteQuery,
|
|
1430
1637
|
rerankContext: config.rerankContext,
|
|
1638
|
+
fallbackFn: config.fallbackFn,
|
|
1431
1639
|
logger
|
|
1432
1640
|
}));
|
|
1433
1641
|
} catch {
|
|
@@ -1480,9 +1688,9 @@ var import_streaming4 = require("hono/streaming");
|
|
|
1480
1688
|
function createAuthMiddleware(deps) {
|
|
1481
1689
|
const { apiKeyManager, logger } = deps;
|
|
1482
1690
|
return async function requirePublicKey(c, next) {
|
|
1483
|
-
const hdr = c.req.header(
|
|
1484
|
-
if (hdr?.startsWith(
|
|
1485
|
-
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0,
|
|
1691
|
+
const hdr = c.req.header(API_KEY_HEADER);
|
|
1692
|
+
if (hdr?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1693
|
+
logger.debug(`[Auth] Session key allowed: ${hdr.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1486
1694
|
await next();
|
|
1487
1695
|
return;
|
|
1488
1696
|
}
|
|
@@ -1492,7 +1700,7 @@ function createAuthMiddleware(deps) {
|
|
|
1492
1700
|
}
|
|
1493
1701
|
if (!hdr) {
|
|
1494
1702
|
logger.debug("[Auth] No API key provided");
|
|
1495
|
-
return c.json({ error:
|
|
1703
|
+
return c.json({ error: `Unauthorized - Missing ${API_KEY_HEADER} header` }, 401);
|
|
1496
1704
|
}
|
|
1497
1705
|
try {
|
|
1498
1706
|
const result = await apiKeyManager.verify(hdr);
|
|
@@ -1506,22 +1714,23 @@ function createAuthMiddleware(deps) {
|
|
|
1506
1714
|
} catch (error) {
|
|
1507
1715
|
logger.warn(`[Auth] JWT verification error: ${error}`);
|
|
1508
1716
|
}
|
|
1509
|
-
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0,
|
|
1717
|
+
logger.debug(`[Auth] Invalid API key: ${hdr?.slice(0, API_KEY_PREVIEW_LENGTH) || "none"}`);
|
|
1510
1718
|
return c.json({ error: "Unauthorized" }, 401);
|
|
1511
1719
|
};
|
|
1512
1720
|
}
|
|
1513
1721
|
|
|
1514
1722
|
// src/middleware/referrer.ts
|
|
1723
|
+
init_logger();
|
|
1515
1724
|
function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsoleLogger()) {
|
|
1516
1725
|
return async (c, next) => {
|
|
1517
|
-
const apiKey = c.req.header(
|
|
1726
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1518
1727
|
const isDemoKey = apiKey ? demoApiKeys.includes(apiKey) : false;
|
|
1519
|
-
const isSessionKey = apiKey?.startsWith(
|
|
1728
|
+
const isSessionKey = apiKey?.startsWith(SESSION_KEY_PREFIX);
|
|
1520
1729
|
if (isDemoKey || isSessionKey) {
|
|
1521
1730
|
const referer = c.req.header("referer");
|
|
1522
1731
|
const origin = c.req.header("origin");
|
|
1523
1732
|
logger.debug(
|
|
1524
|
-
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0,
|
|
1733
|
+
`[Referrer] Checking ${isSessionKey ? "session" : "demo"} key: ${apiKey?.slice(0, API_KEY_PREVIEW_LENGTH)}...`
|
|
1525
1734
|
);
|
|
1526
1735
|
logger.debug(`[Referrer] Origin: ${origin || "none"}, Referer: ${referer || "none"}`);
|
|
1527
1736
|
const isAllowed = matchesAllowedOrigin(origin, referer, allowedOrigins);
|
|
@@ -1541,11 +1750,12 @@ function requireReferrer(allowedOrigins, demoApiKeys = [], logger = createConsol
|
|
|
1541
1750
|
}
|
|
1542
1751
|
|
|
1543
1752
|
// src/middleware/session.ts
|
|
1753
|
+
init_logger();
|
|
1544
1754
|
function validateSessionKey(logger = createConsoleLogger()) {
|
|
1545
1755
|
return async (c, next) => {
|
|
1546
|
-
const apiKey = c.req.header(
|
|
1547
|
-
if (apiKey?.startsWith(
|
|
1548
|
-
logger.debug(`[Session] Validating key: ${apiKey.slice(0,
|
|
1756
|
+
const apiKey = c.req.header(API_KEY_HEADER);
|
|
1757
|
+
if (apiKey?.startsWith(SESSION_KEY_PREFIX)) {
|
|
1758
|
+
logger.debug(`[Session] Validating key: ${apiKey.slice(0, API_KEY_PREVIEW_LENGTH)}...`);
|
|
1549
1759
|
const info = getSessionInfo(apiKey);
|
|
1550
1760
|
if (info) {
|
|
1551
1761
|
logger.debug(
|
|
@@ -1609,6 +1819,7 @@ function publicRoutes(deps) {
|
|
|
1609
1819
|
buckets,
|
|
1610
1820
|
rewriteQuery: config.rewriteQuery,
|
|
1611
1821
|
rerankContext: config.rerankContext,
|
|
1822
|
+
fallbackFn: config.fallbackFn,
|
|
1612
1823
|
logger
|
|
1613
1824
|
}));
|
|
1614
1825
|
} catch {
|
|
@@ -1648,8 +1859,7 @@ function publicRoutes(deps) {
|
|
|
1648
1859
|
async function createServer(config) {
|
|
1649
1860
|
const logger = resolveLogger(config.logger, config.logLevel);
|
|
1650
1861
|
logger.info(`\u{1F680} Starting ${config.bot.name}...`);
|
|
1651
|
-
const
|
|
1652
|
-
const promptsDir = config.promptsDir || "./config/prompts";
|
|
1862
|
+
const promptsDir = config.promptsDir || DEFAULT_PROMPTS_DIR;
|
|
1653
1863
|
const publicDir = config.publicDir || "./public";
|
|
1654
1864
|
const enablePublic = config.features?.enablePublicChat !== false;
|
|
1655
1865
|
const enablePrivate = config.features?.enablePrivateChat !== false;
|
|
@@ -1660,16 +1870,26 @@ async function createServer(config) {
|
|
|
1660
1870
|
const client = new import_openai.default({
|
|
1661
1871
|
apiKey: config.openai.apiKey
|
|
1662
1872
|
});
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1873
|
+
let db;
|
|
1874
|
+
if (config.database) {
|
|
1875
|
+
const { openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1876
|
+
db = await openLibsqlClient2(config.database);
|
|
1877
|
+
}
|
|
1878
|
+
let store;
|
|
1879
|
+
if (config.retriever) {
|
|
1880
|
+
store = config.retriever;
|
|
1881
|
+
} else {
|
|
1882
|
+
if (!db) {
|
|
1883
|
+
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
1884
|
+
}
|
|
1885
|
+
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1886
|
+
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
1887
|
+
databaseClient: db,
|
|
1888
|
+
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
1889
|
+
logger
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
await store.build?.();
|
|
1673
1893
|
logger.info("\u2705 Knowledge base ready");
|
|
1674
1894
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
1675
1895
|
let apiKeyManager;
|
|
@@ -1757,6 +1977,9 @@ async function createServer(config) {
|
|
|
1757
1977
|
const deps = {
|
|
1758
1978
|
client,
|
|
1759
1979
|
store,
|
|
1980
|
+
// See ServerDependencies.db's doc comment: undefined here (cast to
|
|
1981
|
+
// satisfy the still-required type) exactly when config.database was
|
|
1982
|
+
// never set - i.e. a config.retriever host with nothing else needing db.
|
|
1760
1983
|
db,
|
|
1761
1984
|
config,
|
|
1762
1985
|
prompts,
|