@diegoaltoworks/chatter 1.0.0 → 2.1.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.
Files changed (77) hide show
  1. package/README.md +3 -1
  2. package/dist/channels/gates.d.ts +21 -2
  3. package/dist/channels/gates.d.ts.map +1 -1
  4. package/dist/channels/index.d.ts +5 -3
  5. package/dist/channels/index.d.ts.map +1 -1
  6. package/dist/channels/index.js +31 -13
  7. package/dist/channels/index.mjs +30 -13
  8. package/dist/channels/matrix/channel.d.ts +2 -0
  9. package/dist/channels/matrix/channel.d.ts.map +1 -1
  10. package/dist/channels/matrix/handler.d.ts +9 -0
  11. package/dist/channels/matrix/handler.d.ts.map +1 -1
  12. package/dist/channels/matrix/index.d.ts +1 -1
  13. package/dist/channels/matrix/index.d.ts.map +1 -1
  14. package/dist/channels/matrix/index.js +75 -20
  15. package/dist/channels/matrix/index.mjs +74 -20
  16. package/dist/channels/matrix/updates.d.ts +8 -2
  17. package/dist/channels/matrix/updates.d.ts.map +1 -1
  18. package/dist/channels/pipeline.d.ts +34 -4
  19. package/dist/channels/pipeline.d.ts.map +1 -1
  20. package/dist/channels/polling.d.ts +11 -0
  21. package/dist/channels/polling.d.ts.map +1 -1
  22. package/dist/channels/telegram/channel.d.ts +2 -0
  23. package/dist/channels/telegram/channel.d.ts.map +1 -1
  24. package/dist/channels/telegram/handler.d.ts +9 -0
  25. package/dist/channels/telegram/handler.d.ts.map +1 -1
  26. package/dist/channels/telegram/index.d.ts +1 -1
  27. package/dist/channels/telegram/index.d.ts.map +1 -1
  28. package/dist/channels/telegram/index.js +73 -24
  29. package/dist/channels/telegram/index.mjs +72 -24
  30. package/dist/channels/telegram/updates.d.ts +8 -2
  31. package/dist/channels/telegram/updates.d.ts.map +1 -1
  32. package/dist/channels/telegram/webhook.d.ts +10 -0
  33. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  34. package/dist/channels/whatsapp/channel.d.ts +8 -0
  35. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  36. package/dist/channels/whatsapp/inbound.d.ts +2 -0
  37. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  38. package/dist/channels/whatsapp/index.js +45 -18
  39. package/dist/channels/whatsapp/index.mjs +45 -18
  40. package/dist/core/answer.d.ts +9 -2
  41. package/dist/core/answer.d.ts.map +1 -1
  42. package/dist/core/chunking.d.ts +31 -0
  43. package/dist/core/chunking.d.ts.map +1 -0
  44. package/dist/core/guardrails.d.ts +11 -1
  45. package/dist/core/guardrails.d.ts.map +1 -1
  46. package/dist/core/llm.d.ts +3 -1
  47. package/dist/core/llm.d.ts.map +1 -1
  48. package/dist/core/retrieval.d.ts +39 -0
  49. package/dist/core/retrieval.d.ts.map +1 -1
  50. package/dist/history/index.js +12 -9
  51. package/dist/history/index.mjs +12 -9
  52. package/dist/index.d.ts +4 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +261 -52
  55. package/dist/index.mjs +254 -47
  56. package/dist/mcp-server.d.ts.map +1 -1
  57. package/dist/mcp-server.js +217 -30
  58. package/dist/mcp-server.mjs +216 -29
  59. package/dist/personas/index.d.ts +1 -1
  60. package/dist/personas/index.d.ts.map +1 -1
  61. package/dist/personas/index.js +21 -7
  62. package/dist/personas/index.mjs +21 -7
  63. package/dist/personas/resolver.d.ts +35 -8
  64. package/dist/personas/resolver.d.ts.map +1 -1
  65. package/dist/personas/timeContext.d.ts.map +1 -1
  66. package/dist/routes/demo.d.ts.map +1 -1
  67. package/dist/routes/openai.d.ts.map +1 -1
  68. package/dist/routes/private.d.ts.map +1 -1
  69. package/dist/routes/public.d.ts.map +1 -1
  70. package/dist/scheduler/index.js +12 -9
  71. package/dist/scheduler/index.mjs +12 -9
  72. package/dist/server.d.ts.map +1 -1
  73. package/dist/server.js +251 -50
  74. package/dist/server.mjs +246 -45
  75. package/dist/types.d.ts +39 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +13 -13
@@ -114,6 +114,126 @@ var init_buildLock = __esm({
114
114
  }
115
115
  });
116
116
 
117
+ // src/core/chunking.ts
118
+ function headingOf(line) {
119
+ const match = HEADING.exec(line);
120
+ if (!match) return void 0;
121
+ const title = (match[2] ?? "").replace(/[ \t]+#+$/, "").replace(/^#+$/, "").trim();
122
+ return { level: match[1].length, title };
123
+ }
124
+ function startsTable(lines, i) {
125
+ return lines[i].includes("|") && i + 1 < lines.length && TABLE_DELIMITER.test(lines[i + 1]);
126
+ }
127
+ function fenceEnd(lines, start) {
128
+ const opener = FENCE.exec(lines[start]);
129
+ if (!opener) return start + 1;
130
+ const marker = opener[1];
131
+ const closer = new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}[ \\t]*$`);
132
+ for (let i = start + 1; i < lines.length; i++) if (closer.test(lines[i])) return i + 1;
133
+ return lines.length;
134
+ }
135
+ function listEnd(lines, start) {
136
+ let end = start + 1;
137
+ for (let i = start + 1; i < lines.length; i++) {
138
+ const line = lines[i];
139
+ if (isBlank(line)) {
140
+ let next = i + 1;
141
+ while (next < lines.length && isBlank(lines[next])) next++;
142
+ if (next < lines.length && (LIST_ITEM.test(lines[next]) || isIndented(lines[next]))) {
143
+ i = next - 1;
144
+ continue;
145
+ }
146
+ break;
147
+ }
148
+ if (headingOf(line) && !isIndented(line)) break;
149
+ if (FENCE.test(line) && !isIndented(line)) break;
150
+ end = i + 1;
151
+ }
152
+ return end;
153
+ }
154
+ function tableEnd(lines, start) {
155
+ let end = start + 2;
156
+ while (end < lines.length && !isBlank(lines[end]) && lines[end].includes("|")) end++;
157
+ return end;
158
+ }
159
+ function paragraphEnd(lines, start) {
160
+ let end = start + 1;
161
+ while (end < lines.length && !isBlank(lines[end]) && !headingOf(lines[end]) && !FENCE.test(lines[end]) && !LIST_ITEM.test(lines[end])) {
162
+ end++;
163
+ }
164
+ return end;
165
+ }
166
+ function scan(text) {
167
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
168
+ const sections = [{ trail: [], blocks: [] }];
169
+ const stack = [];
170
+ let i = 0;
171
+ while (i < lines.length) {
172
+ const line = lines[i];
173
+ if (isBlank(line)) {
174
+ i++;
175
+ continue;
176
+ }
177
+ const heading = FENCE.test(line) ? void 0 : headingOf(line);
178
+ if (heading) {
179
+ while (stack.length && stack[stack.length - 1].level >= heading.level) stack.pop();
180
+ stack.push(heading);
181
+ sections.push({ trail: stack.map((h) => h.title), heading: line, blocks: [] });
182
+ i++;
183
+ continue;
184
+ }
185
+ let end;
186
+ if (FENCE.test(line)) end = fenceEnd(lines, i);
187
+ else if (LIST_ITEM.test(line)) end = listEnd(lines, i);
188
+ else if (startsTable(lines, i)) end = tableEnd(lines, i);
189
+ else end = paragraphEnd(lines, i);
190
+ sections[sections.length - 1].blocks.push({ lines: lines.slice(i, end) });
191
+ i = end;
192
+ }
193
+ return sections;
194
+ }
195
+ function chunkSections(text, max = 900) {
196
+ const cap = Math.max(1, max);
197
+ const out = [];
198
+ const emit = (parts, section) => {
199
+ const body = parts.join("\n\n");
200
+ if (body.trim() === "") return;
201
+ out.push({ text: body, section: [...section], position: out.length });
202
+ };
203
+ for (const section of scan(text)) {
204
+ let parts = section.heading ? [section.heading.trim()] : [];
205
+ let size = parts.join("\n\n").length;
206
+ let hasBlock = false;
207
+ for (const block of section.blocks) {
208
+ const piece = block.lines.join("\n");
209
+ const added = piece.length + (parts.length ? 2 : 0);
210
+ if (hasBlock && size + added > cap) {
211
+ emit(parts, section.trail);
212
+ parts = [];
213
+ size = 0;
214
+ hasBlock = false;
215
+ }
216
+ size += piece.length + (parts.length ? 2 : 0);
217
+ parts.push(piece);
218
+ hasBlock = true;
219
+ }
220
+ emit(parts, section.trail);
221
+ }
222
+ return out;
223
+ }
224
+ var FENCE, HEADING, LIST_ITEM, TABLE_DELIMITER, isBlank, isIndented;
225
+ var init_chunking = __esm({
226
+ "src/core/chunking.ts"() {
227
+ "use strict";
228
+ FENCE = /^ {0,3}(`{3,}|~{3,})/;
229
+ HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
230
+ LIST_ITEM = /^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+\S/;
231
+ TABLE_DELIMITER = /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
232
+ isBlank = (line) => line.trim() === "";
233
+ isIndented = (line) => /^(?: {2,}|\t)/.test(line);
234
+ }
235
+ });
236
+
117
237
  // src/core/loaders.ts
118
238
  function walk(dir) {
119
239
  const items = [];
@@ -194,11 +314,13 @@ async function sha256(input) {
194
314
  const hash = await crypto.subtle.digest("SHA-256", data);
195
315
  return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
196
316
  }
197
- var EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
317
+ var import_node_path3, EMB_MODEL, DEFAULT_KNOWLEDGE_DIR, DATABASE_CONFIG_REQUIRED_MESSAGE, VectorStore;
198
318
  var init_retrieval = __esm({
199
319
  "src/core/retrieval.ts"() {
200
320
  "use strict";
321
+ import_node_path3 = require("node:path");
201
322
  init_buildLock();
323
+ init_chunking();
202
324
  init_loaders();
203
325
  init_logger();
204
326
  EMB_MODEL = "text-embedding-3-large";
@@ -209,6 +331,7 @@ var init_retrieval = __esm({
209
331
  this.embed = embed;
210
332
  this.db = options.databaseClient;
211
333
  this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
334
+ this.chunking = options.chunking ?? "lines";
212
335
  this.logger = options.logger ?? createConsoleLogger();
213
336
  this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
214
337
  this.instanceId = options.instanceId ?? crypto.randomUUID();
@@ -222,6 +345,7 @@ var init_retrieval = __esm({
222
345
  */
223
346
  db;
224
347
  knowledgeDir;
348
+ chunking;
225
349
  logger;
226
350
  buildLock;
227
351
  instanceId;
@@ -282,11 +406,41 @@ var init_retrieval = __esm({
282
406
  this.logger.info("No knowledge documents and no existing chunks - nothing to build");
283
407
  return;
284
408
  }
409
+ this.logger.info(`Chunking mode: ${this.chunking}`);
410
+ if (this.chunking === "sections") await this.ensureSectionColumns();
285
411
  const rows = [];
286
412
  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 });
413
+ if (this.chunking === "sections") {
414
+ const source = (0, import_node_path3.relative)(this.knowledgeDir, d.source).split(import_node_path3.sep).join("/");
415
+ for (const c of chunkSections(d.text)) {
416
+ const section = c.section.join(" > ");
417
+ const id = await sha256(`${d.bucket}|${source}|${section}|${c.text}`);
418
+ const context = section ? `${source} > ${section}` : source;
419
+ rows.push({
420
+ id,
421
+ bucket: d.bucket,
422
+ source,
423
+ text: c.text,
424
+ embedText: `${context}
425
+
426
+ ${c.text}`,
427
+ section,
428
+ position: c.position
429
+ });
430
+ }
431
+ } else {
432
+ for (const part of chunk(d.text)) {
433
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
434
+ rows.push({
435
+ id,
436
+ bucket: d.bucket,
437
+ source: d.source,
438
+ text: part,
439
+ embedText: part,
440
+ section: null,
441
+ position: null
442
+ });
443
+ }
290
444
  }
291
445
  }
292
446
  this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
@@ -295,11 +449,17 @@ var init_retrieval = __esm({
295
449
  for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
296
450
  const batch = rows.slice(i, i + UPSERT_BATCH);
297
451
  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
- })),
452
+ batch.map(
453
+ (r) => this.chunking === "sections" ? {
454
+ sql: `INSERT INTO chunks(id,bucket,source,text,section,position) VALUES(?,?,?,?,?,?)
455
+ ON CONFLICT(id) DO NOTHING`,
456
+ args: [r.id, r.bucket, r.source, r.text, r.section, r.position]
457
+ } : {
458
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
459
+ ON CONFLICT(id) DO NOTHING`,
460
+ args: [r.id, r.bucket, r.source, r.text]
461
+ }
462
+ ),
303
463
  "write"
304
464
  );
305
465
  }
@@ -320,7 +480,7 @@ var init_retrieval = __esm({
320
480
  return;
321
481
  }
322
482
  this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
323
- const textById = new Map(rows.map((r) => [r.id, r.text]));
483
+ const textById = new Map(rows.map((r) => [r.id, r.embedText]));
324
484
  const BATCH = 96;
325
485
  for (let i = 0; i < missing.length; i += BATCH) {
326
486
  const batchIds = missing.slice(i, i + BATCH);
@@ -341,6 +501,18 @@ var init_retrieval = __esm({
341
501
  }
342
502
  this.logger.info(`Successfully embedded ${missing.length} new chunks`);
343
503
  }
504
+ /**
505
+ * Adds the nullable `section`/`position` columns to a `chunks` table that
506
+ * every released version created without them. Checked via table_info so it
507
+ * is a no-op once present; only `'sections'` mode ever calls it.
508
+ */
509
+ async ensureSectionColumns() {
510
+ const info = await this.db.execute("PRAGMA table_info(chunks)");
511
+ const have = new Set(info.rows.map((r) => String(r.name)));
512
+ if (!have.has("section")) await this.db.execute("ALTER TABLE chunks ADD COLUMN section TEXT");
513
+ if (!have.has("position"))
514
+ await this.db.execute("ALTER TABLE chunks ADD COLUMN position INTEGER");
515
+ }
344
516
  // Remove chunks from database that no longer exist in markdown files
345
517
  async cleanupStaleChunks(currentIds) {
346
518
  const result = await this.db.execute("SELECT id FROM chunks");
@@ -387,25 +559,34 @@ var init_retrieval = __esm({
387
559
  * interpolated. An empty list retrieves nothing, and short-circuits before
388
560
  * the embedding call.
389
561
  */
390
- async query(q, k = 6, allowed = ["base"]) {
562
+ async queryChunks(q, k = 6, allowed = ["base"]) {
391
563
  if (allowed.length === 0) return [];
392
564
  const [qv] = await this.embed([q]);
393
565
  const placeholders = allowed.map(() => "?").join(",");
394
566
  const res = await this.db.execute({
395
- sql: `SELECT c.id, c.text, e.embedding
567
+ sql: `SELECT c.*, e.embedding
396
568
  FROM chunks c
397
569
  JOIN embeddings e ON e.id = c.id
398
570
  WHERE c.bucket IN (${placeholders})`,
399
571
  args: allowed
400
572
  });
401
- const scored = [];
402
- for (const row of res.rows) {
573
+ const scored = res.rows.map((row) => {
403
574
  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);
575
+ const trail = row.section == null ? "" : String(row.section);
576
+ return {
577
+ text: String(row.text),
578
+ bucket: String(row.bucket ?? ""),
579
+ source: String(row.source ?? ""),
580
+ section: trail ? trail.split(" > ") : [],
581
+ score: _VectorStore.cosine(qv, emb)
582
+ };
583
+ });
584
+ scored.sort((a, b) => b.score - a.score);
585
+ return scored.slice(0, k);
586
+ }
587
+ /** Same results as {@link queryChunks}, as bare text. */
588
+ async query(q, k = 6, allowed = ["base"]) {
589
+ return (await this.queryChunks(q, k, allowed)).map((c) => c.text);
409
590
  }
410
591
  };
411
592
  }
@@ -546,9 +727,10 @@ function detectLeakage(text) {
546
727
  function scrubOutput(text) {
547
728
  return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
548
729
  }
549
- function guardOutput(text) {
730
+ var DEFAULT_REFUSAL = "Sorry, I can't share internal instructions. How else can I help?";
731
+ function guardOutput(text, refusal) {
550
732
  if (detectLeakage(text)) {
551
- return "Sorry, I can't share internal instructions. How else can I help?";
733
+ return typeof refusal === "string" && refusal.trim() ? refusal : DEFAULT_REFUSAL;
552
734
  }
553
735
  return scrubOutput(text);
554
736
  }
@@ -560,7 +742,8 @@ async function completeOnce({
560
742
  system,
561
743
  messages,
562
744
  temperature = 0.2,
563
- model = DEFAULT_MODEL
745
+ model = DEFAULT_MODEL,
746
+ refusal
564
747
  }) {
565
748
  const res = await client.chat.completions.create({
566
749
  model,
@@ -569,19 +752,19 @@ async function completeOnce({
569
752
  });
570
753
  const text = res.choices[0]?.message?.content ?? "";
571
754
  return {
572
- content: guardOutput(text),
755
+ content: guardOutput(text, refusal),
573
756
  usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
574
757
  };
575
758
  }
576
759
 
577
760
  // src/core/answer.ts
578
761
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
579
- async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
762
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal) {
580
763
  const result = await answerFn({ system, messages, mode, sender, conversationId });
581
764
  const raw = typeof result === "string" ? result : result?.content;
582
765
  const usage = typeof result === "string" ? void 0 : result?.usage;
583
766
  return {
584
- content: guardOutput(typeof raw === "string" ? raw : ""),
767
+ content: guardOutput(typeof raw === "string" ? raw : "", refusal),
585
768
  usage: usage ?? noUsage()
586
769
  };
587
770
  }
@@ -594,12 +777,13 @@ async function answerOnce({
594
777
  sender,
595
778
  conversationId,
596
779
  temperature,
597
- model
780
+ model,
781
+ refusal
598
782
  }) {
599
783
  if (answerFn) {
600
- return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
784
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal);
601
785
  }
602
- return completeOnce({ client, system, messages, temperature, model });
786
+ return completeOnce({ client, system, messages, temperature, model, refusal });
603
787
  }
604
788
  async function applyTransformReply(transformReply, input, logger) {
605
789
  if (!transformReply) return input.text;
@@ -1029,6 +1213,7 @@ async function createMCPServer(config) {
1029
1213
  store = new VectorStore2(createOpenAIEmbedder2(client), {
1030
1214
  databaseClient: db,
1031
1215
  knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
1216
+ chunking: config.chunking,
1032
1217
  logger: log
1033
1218
  });
1034
1219
  }
@@ -1100,7 +1285,8 @@ async function createMCPServer(config) {
1100
1285
  messages: conversationMessages,
1101
1286
  mode: "public",
1102
1287
  conversationId: convId,
1103
- model
1288
+ model,
1289
+ refusal: config.refusal
1104
1290
  });
1105
1291
  const replyText = await applyTransformReply(
1106
1292
  config.transformReply,
@@ -1190,7 +1376,8 @@ async function createMCPServer(config) {
1190
1376
  messages: conversationMessages,
1191
1377
  mode: "private",
1192
1378
  conversationId: convId,
1193
- model
1379
+ model,
1380
+ refusal: config.refusal
1194
1381
  });
1195
1382
  const replyText = await applyTransformReply(
1196
1383
  config.transformReply,