@diegoaltoworks/chatter 0.59.0 → 0.61.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 +2 -1
- package/dist/channels/index.js +81 -0
- package/dist/channels/index.mjs +81 -0
- package/dist/channels/matrix/api.d.ts +133 -0
- package/dist/channels/matrix/api.d.ts.map +1 -0
- package/dist/channels/matrix/channel.d.ts +98 -0
- package/dist/channels/matrix/channel.d.ts.map +1 -0
- package/dist/channels/matrix/handler.d.ts +51 -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 +972 -0
- package/dist/channels/matrix/index.mjs +929 -0
- package/dist/channels/matrix/sync.d.ts +50 -0
- package/dist/channels/matrix/sync.d.ts.map +1 -0
- package/dist/channels/matrix/updates.d.ts +75 -0
- package/dist/channels/matrix/updates.d.ts.map +1 -0
- package/dist/channels/pipeline.d.ts +8 -0
- package/dist/channels/pipeline.d.ts.map +1 -1
- package/dist/channels/telegram/channel.d.ts +7 -0
- package/dist/channels/telegram/channel.d.ts.map +1 -1
- package/dist/channels/telegram/index.js +81 -0
- package/dist/channels/telegram/index.mjs +81 -0
- package/dist/channels/telegram/webhook.d.ts +7 -0
- package/dist/channels/telegram/webhook.d.ts.map +1 -1
- package/dist/channels/whatsapp/inbound.d.ts +7 -0
- package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +81 -0
- package/dist/channels/whatsapp/index.mjs +81 -0
- package/dist/history/compaction.d.ts +61 -0
- package/dist/history/compaction.d.ts.map +1 -0
- package/dist/history/index.d.ts +7 -0
- package/dist/history/index.d.ts.map +1 -1
- package/dist/history/index.js +143 -0
- package/dist/history/index.mjs +141 -0
- package/package.json +8 -2
|
@@ -778,6 +778,75 @@ ${ctx.join("\n\n")}`
|
|
|
778
778
|
return { system, messages, context: ctx };
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
// src/history/compaction.ts
|
|
782
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
783
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
784
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
785
|
+
function defaultSummarize(client, model) {
|
|
786
|
+
return async ({ turns }) => {
|
|
787
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
788
|
+
const { content } = await answerOnce({
|
|
789
|
+
client,
|
|
790
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
791
|
+
messages: [{ role: "user", content: transcript }],
|
|
792
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
793
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
794
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
795
|
+
mode: "public",
|
|
796
|
+
model
|
|
797
|
+
});
|
|
798
|
+
return content;
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function createHistoryCompactor(deps, options) {
|
|
802
|
+
const { threshold, keep, logger } = options;
|
|
803
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
804
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
805
|
+
}
|
|
806
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
807
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
808
|
+
}
|
|
809
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
810
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
811
|
+
return {
|
|
812
|
+
async maybeCompact(store, conversationId) {
|
|
813
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
814
|
+
if (turns.length < threshold) return;
|
|
815
|
+
const older = turns.slice(0, turns.length - keep);
|
|
816
|
+
let summary;
|
|
817
|
+
let timer;
|
|
818
|
+
try {
|
|
819
|
+
summary = await Promise.race([
|
|
820
|
+
summarize({ conversationId, turns: older }),
|
|
821
|
+
new Promise((_, reject) => {
|
|
822
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
823
|
+
})
|
|
824
|
+
]);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
logger?.error(
|
|
827
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
828
|
+
error
|
|
829
|
+
);
|
|
830
|
+
return;
|
|
831
|
+
} finally {
|
|
832
|
+
clearTimeout(timer);
|
|
833
|
+
}
|
|
834
|
+
const trimmed = summary.trim();
|
|
835
|
+
if (!trimmed) return;
|
|
836
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
837
|
+
const recent = fresh.slice(older.length);
|
|
838
|
+
await store.clear(conversationId);
|
|
839
|
+
await store.append(conversationId, {
|
|
840
|
+
role: "assistant",
|
|
841
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
842
|
+
});
|
|
843
|
+
for (const turn of recent) {
|
|
844
|
+
await store.append(conversationId, turn);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
781
850
|
// src/channels/pipeline.ts
|
|
782
851
|
var DEFAULT_HISTORY_LIMIT = 20;
|
|
783
852
|
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
@@ -808,6 +877,10 @@ function createInboundPipeline(deps, config) {
|
|
|
808
877
|
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
809
878
|
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
810
879
|
const mode = "public";
|
|
880
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
881
|
+
{ client: deps.client },
|
|
882
|
+
{ model: config.model, ...config.history.compaction }
|
|
883
|
+
) : void 0;
|
|
811
884
|
return async function handleInboundMessage(msg, turn) {
|
|
812
885
|
const allowedChats = config.allowedChats ?? [];
|
|
813
886
|
const action = decideChannelAction(msg, {
|
|
@@ -883,6 +956,14 @@ function createInboundPipeline(deps, config) {
|
|
|
883
956
|
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
884
957
|
}
|
|
885
958
|
await turn.reply.sendAnswer(msg.chatId, content);
|
|
959
|
+
const history = config.history;
|
|
960
|
+
if (compactor && historyEnabled && history) {
|
|
961
|
+
try {
|
|
962
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
886
967
|
return { action: "reply", content };
|
|
887
968
|
};
|
|
888
969
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Summarize-then-truncate compaction for `HistoryStore` — folds older turns
|
|
3
|
+
* into a single stored summary row once a conversation's window grows past a
|
|
4
|
+
* threshold, so a long-running conversation's prompt doesn't grow without
|
|
5
|
+
* bound. Off by default; a caller wires `maybeCompact` in right after its own
|
|
6
|
+
* `store.append` call, the same way `./index`'s docs describe wiring the
|
|
7
|
+
* store itself.
|
|
8
|
+
*
|
|
9
|
+
* The summarize step races a timeout the same way `../images/captionComposer`
|
|
10
|
+
* races its compose step, except the safe fallback here is "leave history
|
|
11
|
+
* untouched" rather than a template pool — a stuck or failing summarize call
|
|
12
|
+
* must never lose turns nobody got to read back.
|
|
13
|
+
*/
|
|
14
|
+
import type OpenAI from "openai";
|
|
15
|
+
import type { Logger } from "../core/logger";
|
|
16
|
+
import type { HistoryMessage, HistoryStore } from "./types";
|
|
17
|
+
/** What a summarize step receives: the turns being folded away, oldest first. Never includes the turns `keep` retains verbatim. */
|
|
18
|
+
export interface SummarizeRequest {
|
|
19
|
+
conversationId: string;
|
|
20
|
+
turns: HistoryMessage[];
|
|
21
|
+
}
|
|
22
|
+
/** Best-effort compaction step, typically an LLM call. */
|
|
23
|
+
export type SummarizeFn = (request: SummarizeRequest) => Promise<string>;
|
|
24
|
+
export interface HistoryCompactionOptions {
|
|
25
|
+
/** Turn count that triggers compaction. */
|
|
26
|
+
threshold: number;
|
|
27
|
+
/** Most recent turns kept verbatim after compaction; everything older is folded into the summary. Must be less than `threshold`. */
|
|
28
|
+
keep: number;
|
|
29
|
+
/**
|
|
30
|
+
* Best-effort summarize step. Defaults to `answerOnce` with a neutral
|
|
31
|
+
* prompt and no `answerFn` (so it always uses the built-in completion,
|
|
32
|
+
* never a caller's brain hook) — set this to route summarization through a
|
|
33
|
+
* caller's own `answerFn`, a cheaper model, or a non-LLM summarizer.
|
|
34
|
+
*/
|
|
35
|
+
summarize?: SummarizeFn;
|
|
36
|
+
/** Model for the default `summarize` step. Ignored when `summarize` is supplied. Falls back to `answerOnce`'s own default model when unset. */
|
|
37
|
+
model?: string;
|
|
38
|
+
/** Milliseconds before `summarize` is abandoned in favour of leaving history untouched for this turn. @default 8000 */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
logger?: Pick<Logger, "error">;
|
|
41
|
+
}
|
|
42
|
+
export interface HistoryCompactor {
|
|
43
|
+
/**
|
|
44
|
+
* Compacts `conversationId` if its stored turn count has reached
|
|
45
|
+
* `threshold`. A no-op below threshold. A failed or timed-out `summarize`
|
|
46
|
+
* leaves the store exactly as it was and does not throw; a failure in the
|
|
47
|
+
* store itself (`load`/`clear`/`append`) propagates like any other store
|
|
48
|
+
* failure.
|
|
49
|
+
*/
|
|
50
|
+
maybeCompact(store: HistoryStore, conversationId: string): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** Marks a stored row as a compaction summary rather than a real turn — informational only, nothing parses it back out. */
|
|
53
|
+
export declare const SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
54
|
+
/**
|
|
55
|
+
* `deps.client` builds the default `summarize` step; unused when a caller
|
|
56
|
+
* supplies its own.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createHistoryCompactor(deps: {
|
|
59
|
+
client: OpenAI;
|
|
60
|
+
}, options: HistoryCompactionOptions): HistoryCompactor;
|
|
61
|
+
//# sourceMappingURL=compaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/history/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5D,mIAAmI;AACnI,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,0DAA0D;AAC1D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,wBAAwB;IACvC,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,oIAAoI;IACpI,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,+IAA+I;IAC/I,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uHAAuH;IACvH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAWD,2HAA2H;AAC3H,eAAO,MAAM,cAAc,sCAAsC,CAAC;AAoBlE;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACxB,OAAO,EAAE,wBAAwB,GAChC,gBAAgB,CAgElB"}
|
package/dist/history/index.d.ts
CHANGED
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
* implement it. The shipped Turso implementation needs `@libsql/client`,
|
|
11
11
|
* which chatter already expects for retrieval.
|
|
12
12
|
*
|
|
13
|
+
* `./compaction` is an optional layer on top: summarize-then-truncate for a
|
|
14
|
+
* conversation whose window has grown past a threshold, built entirely on
|
|
15
|
+
* the store's own `load`/`clear`/`append` so it works with any
|
|
16
|
+
* `HistoryStore`, not just the Turso one.
|
|
17
|
+
*
|
|
13
18
|
* @packageDocumentation
|
|
14
19
|
*/
|
|
20
|
+
export type { HistoryCompactionOptions, HistoryCompactor, SummarizeFn, SummarizeRequest, } from "./compaction";
|
|
21
|
+
export { createHistoryCompactor, SUMMARY_PREFIX } from "./compaction";
|
|
15
22
|
export type { TursoHistoryStoreOptions } from "./tursoStore";
|
|
16
23
|
export { createTursoHistoryStore } from "./tursoStore";
|
|
17
24
|
export type { HistoryMessage, HistoryStore } from "./types";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,YAAY,EACV,wBAAwB,EACxB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/history/index.js
CHANGED
|
@@ -20,10 +20,151 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/history/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
SUMMARY_PREFIX: () => SUMMARY_PREFIX,
|
|
24
|
+
createHistoryCompactor: () => createHistoryCompactor,
|
|
23
25
|
createTursoHistoryStore: () => createTursoHistoryStore
|
|
24
26
|
});
|
|
25
27
|
module.exports = __toCommonJS(index_exports);
|
|
26
28
|
|
|
29
|
+
// src/core/guardrails.ts
|
|
30
|
+
function detectLeakage(text) {
|
|
31
|
+
const flags = [
|
|
32
|
+
/system prompt/i,
|
|
33
|
+
/hidden instruction/i,
|
|
34
|
+
/here (are|is) (my|the) rules/i,
|
|
35
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
36
|
+
/tool instructions/i
|
|
37
|
+
];
|
|
38
|
+
return flags.some((rx) => rx.test(text));
|
|
39
|
+
}
|
|
40
|
+
function scrubOutput(text) {
|
|
41
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
42
|
+
}
|
|
43
|
+
function guardOutput(text) {
|
|
44
|
+
if (detectLeakage(text)) {
|
|
45
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
46
|
+
}
|
|
47
|
+
return scrubOutput(text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/core/llm.ts
|
|
51
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
52
|
+
async function completeOnce({
|
|
53
|
+
client,
|
|
54
|
+
system,
|
|
55
|
+
messages,
|
|
56
|
+
temperature = 0.2,
|
|
57
|
+
model = DEFAULT_MODEL
|
|
58
|
+
}) {
|
|
59
|
+
const res = await client.chat.completions.create({
|
|
60
|
+
model,
|
|
61
|
+
temperature,
|
|
62
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
63
|
+
});
|
|
64
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
65
|
+
return {
|
|
66
|
+
content: guardOutput(text),
|
|
67
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/core/answer.ts
|
|
72
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
73
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
|
|
74
|
+
const result = await answerFn({ system, messages, mode, sender, conversationId });
|
|
75
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
76
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
77
|
+
return {
|
|
78
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
79
|
+
usage: usage ?? noUsage()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function answerOnce({
|
|
83
|
+
answerFn,
|
|
84
|
+
client,
|
|
85
|
+
system,
|
|
86
|
+
messages,
|
|
87
|
+
mode,
|
|
88
|
+
sender,
|
|
89
|
+
conversationId,
|
|
90
|
+
temperature,
|
|
91
|
+
model
|
|
92
|
+
}) {
|
|
93
|
+
if (answerFn) {
|
|
94
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
|
|
95
|
+
}
|
|
96
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/history/compaction.ts
|
|
100
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
101
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
102
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
103
|
+
function defaultSummarize(client, model) {
|
|
104
|
+
return async ({ turns }) => {
|
|
105
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
106
|
+
const { content } = await answerOnce({
|
|
107
|
+
client,
|
|
108
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
109
|
+
messages: [{ role: "user", content: transcript }],
|
|
110
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
111
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
112
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
113
|
+
mode: "public",
|
|
114
|
+
model
|
|
115
|
+
});
|
|
116
|
+
return content;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function createHistoryCompactor(deps, options) {
|
|
120
|
+
const { threshold, keep, logger } = options;
|
|
121
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
122
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
123
|
+
}
|
|
124
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
125
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
126
|
+
}
|
|
127
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
128
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
129
|
+
return {
|
|
130
|
+
async maybeCompact(store, conversationId) {
|
|
131
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
132
|
+
if (turns.length < threshold) return;
|
|
133
|
+
const older = turns.slice(0, turns.length - keep);
|
|
134
|
+
let summary;
|
|
135
|
+
let timer;
|
|
136
|
+
try {
|
|
137
|
+
summary = await Promise.race([
|
|
138
|
+
summarize({ conversationId, turns: older }),
|
|
139
|
+
new Promise((_, reject) => {
|
|
140
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
141
|
+
})
|
|
142
|
+
]);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger?.error(
|
|
145
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
146
|
+
error
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
const trimmed = summary.trim();
|
|
153
|
+
if (!trimmed) return;
|
|
154
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
155
|
+
const recent = fresh.slice(older.length);
|
|
156
|
+
await store.clear(conversationId);
|
|
157
|
+
await store.append(conversationId, {
|
|
158
|
+
role: "assistant",
|
|
159
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
160
|
+
});
|
|
161
|
+
for (const turn of recent) {
|
|
162
|
+
await store.append(conversationId, turn);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
27
168
|
// src/history/tursoStore.ts
|
|
28
169
|
var schemaReady = /* @__PURE__ */ new WeakMap();
|
|
29
170
|
function ensureHistorySchema(client, tableName) {
|
|
@@ -117,5 +258,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
|
|
|
117
258
|
}
|
|
118
259
|
// Annotate the CommonJS export names for ESM import in node:
|
|
119
260
|
0 && (module.exports = {
|
|
261
|
+
SUMMARY_PREFIX,
|
|
262
|
+
createHistoryCompactor,
|
|
120
263
|
createTursoHistoryStore
|
|
121
264
|
});
|
package/dist/history/index.mjs
CHANGED
|
@@ -1,3 +1,142 @@
|
|
|
1
|
+
// src/core/guardrails.ts
|
|
2
|
+
function detectLeakage(text) {
|
|
3
|
+
const flags = [
|
|
4
|
+
/system prompt/i,
|
|
5
|
+
/hidden instruction/i,
|
|
6
|
+
/here (are|is) (my|the) rules/i,
|
|
7
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
8
|
+
/tool instructions/i
|
|
9
|
+
];
|
|
10
|
+
return flags.some((rx) => rx.test(text));
|
|
11
|
+
}
|
|
12
|
+
function scrubOutput(text) {
|
|
13
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
14
|
+
}
|
|
15
|
+
function guardOutput(text) {
|
|
16
|
+
if (detectLeakage(text)) {
|
|
17
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
18
|
+
}
|
|
19
|
+
return scrubOutput(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/core/llm.ts
|
|
23
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
24
|
+
async function completeOnce({
|
|
25
|
+
client,
|
|
26
|
+
system,
|
|
27
|
+
messages,
|
|
28
|
+
temperature = 0.2,
|
|
29
|
+
model = DEFAULT_MODEL
|
|
30
|
+
}) {
|
|
31
|
+
const res = await client.chat.completions.create({
|
|
32
|
+
model,
|
|
33
|
+
temperature,
|
|
34
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
35
|
+
});
|
|
36
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
37
|
+
return {
|
|
38
|
+
content: guardOutput(text),
|
|
39
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/core/answer.ts
|
|
44
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
45
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
|
|
46
|
+
const result = await answerFn({ system, messages, mode, sender, conversationId });
|
|
47
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
48
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
49
|
+
return {
|
|
50
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
51
|
+
usage: usage ?? noUsage()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function answerOnce({
|
|
55
|
+
answerFn,
|
|
56
|
+
client,
|
|
57
|
+
system,
|
|
58
|
+
messages,
|
|
59
|
+
mode,
|
|
60
|
+
sender,
|
|
61
|
+
conversationId,
|
|
62
|
+
temperature,
|
|
63
|
+
model
|
|
64
|
+
}) {
|
|
65
|
+
if (answerFn) {
|
|
66
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
|
|
67
|
+
}
|
|
68
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/history/compaction.ts
|
|
72
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
73
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
74
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
75
|
+
function defaultSummarize(client, model) {
|
|
76
|
+
return async ({ turns }) => {
|
|
77
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
78
|
+
const { content } = await answerOnce({
|
|
79
|
+
client,
|
|
80
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
81
|
+
messages: [{ role: "user", content: transcript }],
|
|
82
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
83
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
84
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
85
|
+
mode: "public",
|
|
86
|
+
model
|
|
87
|
+
});
|
|
88
|
+
return content;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function createHistoryCompactor(deps, options) {
|
|
92
|
+
const { threshold, keep, logger } = options;
|
|
93
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
94
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
95
|
+
}
|
|
96
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
97
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
98
|
+
}
|
|
99
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
100
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
101
|
+
return {
|
|
102
|
+
async maybeCompact(store, conversationId) {
|
|
103
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
104
|
+
if (turns.length < threshold) return;
|
|
105
|
+
const older = turns.slice(0, turns.length - keep);
|
|
106
|
+
let summary;
|
|
107
|
+
let timer;
|
|
108
|
+
try {
|
|
109
|
+
summary = await Promise.race([
|
|
110
|
+
summarize({ conversationId, turns: older }),
|
|
111
|
+
new Promise((_, reject) => {
|
|
112
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
113
|
+
})
|
|
114
|
+
]);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger?.error(
|
|
117
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
118
|
+
error
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
} finally {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
}
|
|
124
|
+
const trimmed = summary.trim();
|
|
125
|
+
if (!trimmed) return;
|
|
126
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
127
|
+
const recent = fresh.slice(older.length);
|
|
128
|
+
await store.clear(conversationId);
|
|
129
|
+
await store.append(conversationId, {
|
|
130
|
+
role: "assistant",
|
|
131
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
132
|
+
});
|
|
133
|
+
for (const turn of recent) {
|
|
134
|
+
await store.append(conversationId, turn);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
1
140
|
// src/history/tursoStore.ts
|
|
2
141
|
var schemaReady = /* @__PURE__ */ new WeakMap();
|
|
3
142
|
function ensureHistorySchema(client, tableName) {
|
|
@@ -90,5 +229,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
|
|
|
90
229
|
};
|
|
91
230
|
}
|
|
92
231
|
export {
|
|
232
|
+
SUMMARY_PREFIX,
|
|
233
|
+
createHistoryCompactor,
|
|
93
234
|
createTursoHistoryStore
|
|
94
235
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegoaltoworks/chatter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -68,6 +68,11 @@
|
|
|
68
68
|
"import": "./dist/channels/telegram/index.mjs",
|
|
69
69
|
"require": "./dist/channels/telegram/index.js"
|
|
70
70
|
},
|
|
71
|
+
"./matrix": {
|
|
72
|
+
"types": "./dist/channels/matrix/index.d.ts",
|
|
73
|
+
"import": "./dist/channels/matrix/index.mjs",
|
|
74
|
+
"require": "./dist/channels/matrix/index.js"
|
|
75
|
+
},
|
|
71
76
|
"./images": {
|
|
72
77
|
"types": "./dist/images/index.d.ts",
|
|
73
78
|
"import": "./dist/images/index.mjs",
|
|
@@ -88,7 +93,7 @@
|
|
|
88
93
|
],
|
|
89
94
|
"scripts": {
|
|
90
95
|
"dev": "bun --watch src/index.ts",
|
|
91
|
-
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:server && bun run build:mcp && bun run build:usage && bun run build:history && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:telegram && bun run build:flows && bun run build:images && bun run build:scheduler && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
96
|
+
"build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:server && bun run build:mcp && bun run build:usage && bun run build:history && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:telegram && bun run build:matrix && bun run build:flows && bun run build:images && bun run build:scheduler && bun run build:bin && bun run build:client && bun run build:copy-client",
|
|
92
97
|
"build:clean": "rm -rf dist",
|
|
93
98
|
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
94
99
|
"build:esm": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs --external:@hono/node-server --external:hono --external:openai --external:@libsql/client --external:jose --external:@modelcontextprotocol/sdk --external:zod --platform=node --target=node24",
|
|
@@ -101,6 +106,7 @@
|
|
|
101
106
|
"build:channels": "esbuild src/channels/index.ts --bundle --format=esm --outfile=dist/channels/index.mjs --external:hono --external:openai --external:@libsql/client --platform=node --target=node24 && esbuild src/channels/index.ts --bundle --format=cjs --outfile=dist/channels/index.js --external:hono --external:openai --external:@libsql/client --platform=node --target=node24",
|
|
102
107
|
"build:whatsapp": "esbuild src/channels/whatsapp/index.ts --bundle --format=esm --outfile=dist/channels/whatsapp/index.mjs --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24 && esbuild src/channels/whatsapp/index.ts --bundle --format=cjs --outfile=dist/channels/whatsapp/index.js --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24",
|
|
103
108
|
"build:telegram": "esbuild src/channels/telegram/index.ts --bundle --format=esm --outfile=dist/channels/telegram/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/channels/telegram/index.ts --bundle --format=cjs --outfile=dist/channels/telegram/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
109
|
+
"build:matrix": "esbuild src/channels/matrix/index.ts --bundle --format=esm --outfile=dist/channels/matrix/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/channels/matrix/index.ts --bundle --format=cjs --outfile=dist/channels/matrix/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
104
110
|
"build:flows": "esbuild src/flows/index.ts --bundle --format=esm --outfile=dist/flows/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/flows/index.ts --bundle --format=cjs --outfile=dist/flows/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
105
111
|
"build:images": "esbuild src/images/index.ts --bundle --format=esm --outfile=dist/images/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/images/index.ts --bundle --format=cjs --outfile=dist/images/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|
|
106
112
|
"build:scheduler": "esbuild src/scheduler/index.ts --bundle --format=esm --outfile=dist/scheduler/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/scheduler/index.ts --bundle --format=cjs --outfile=dist/scheduler/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
|