telegram-agent-memory 0.2.1
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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/intelligence/AnthropicProvider.d.ts +20 -0
- package/dist/intelligence/AnthropicProvider.js +64 -0
- package/dist/intelligence/OpenAIProvider.d.ts +16 -0
- package/dist/intelligence/OpenAIProvider.js +51 -0
- package/dist/intelligence/logger.d.ts +7 -0
- package/dist/intelligence/logger.js +6 -0
- package/dist/intelligence/types.d.ts +18 -0
- package/dist/intelligence/types.js +2 -0
- package/dist/telegram/TelegramMemory.d.ts +174 -0
- package/dist/telegram/TelegramMemory.js +981 -0
- package/dist/telegram/file-memory.d.ts +72 -0
- package/dist/telegram/file-memory.js +325 -0
- package/dist/telegram/local-index.d.ts +53 -0
- package/dist/telegram/local-index.js +130 -0
- package/package.json +68 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TelegramMemory = exports.TelegramMemoryConfigError = void 0;
|
|
7
|
+
exports.createTelegramMemory = createTelegramMemory;
|
|
8
|
+
exports.validateTelegramMemoryConfig = validateTelegramMemoryConfig;
|
|
9
|
+
exports.assertValidTelegramMemoryConfig = assertValidTelegramMemoryConfig;
|
|
10
|
+
exports.telegramMemoryConfigFromEnv = telegramMemoryConfigFromEnv;
|
|
11
|
+
exports.createTelegramMemoryFromEnv = createTelegramMemoryFromEnv;
|
|
12
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
14
|
+
const node_crypto_1 = require("node:crypto");
|
|
15
|
+
const AnthropicProvider_1 = require("../intelligence/AnthropicProvider");
|
|
16
|
+
const OpenAIProvider_1 = require("../intelligence/OpenAIProvider");
|
|
17
|
+
const file_memory_1 = require("./file-memory");
|
|
18
|
+
const local_index_1 = require("./local-index");
|
|
19
|
+
class TelegramMemoryConfigError extends Error {
|
|
20
|
+
constructor(message, issues) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'TelegramMemoryConfigError';
|
|
23
|
+
this.issues = issues;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.TelegramMemoryConfigError = TelegramMemoryConfigError;
|
|
27
|
+
const DEFAULT_STORAGE_DIR = node_path_1.default.resolve(process.cwd(), '.telegram-agent-memory');
|
|
28
|
+
const DEFAULT_RECENT_DAYS = 3;
|
|
29
|
+
const DEFAULT_MAX_PROFILE_FACTS = 40;
|
|
30
|
+
const DEFAULT_MAX_FACTS_PER_MESSAGE = 8;
|
|
31
|
+
const DEFAULT_PROFILE_CONTEXT_FACTS = 6;
|
|
32
|
+
const DEFAULT_DAILY_CONTEXT_FACTS = 5;
|
|
33
|
+
const DAILY_RECALL_MIN_SCORE = 0.45;
|
|
34
|
+
const DEFAULT_SHARE_PROFILE_ACROSS_GROUPS = true;
|
|
35
|
+
function clampImportance(value) {
|
|
36
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
37
|
+
return 0.5;
|
|
38
|
+
}
|
|
39
|
+
return Math.max(0, Math.min(1, value));
|
|
40
|
+
}
|
|
41
|
+
function normalizeTags(tags) {
|
|
42
|
+
if (!Array.isArray(tags)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return Array.from(new Set(tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean))).slice(0, 8);
|
|
46
|
+
}
|
|
47
|
+
function normalizeCategory(value) {
|
|
48
|
+
const trimmed = value?.trim().toLowerCase();
|
|
49
|
+
return trimmed || 'other';
|
|
50
|
+
}
|
|
51
|
+
function scoreFact(fact, query) {
|
|
52
|
+
const text = `${fact.summary}\n${fact.content}\n${fact.tags.join(' ')}`.toLowerCase();
|
|
53
|
+
const tokens = query
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.split(/\s+/)
|
|
56
|
+
.map((item) => item.trim())
|
|
57
|
+
.filter(Boolean);
|
|
58
|
+
let matchCount = 0;
|
|
59
|
+
for (const token of tokens) {
|
|
60
|
+
if (text.includes(token)) {
|
|
61
|
+
matchCount += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const lexical = tokens.length === 0 ? 0.3 : matchCount / tokens.length;
|
|
65
|
+
return lexical * 0.7 + fact.importance * 0.3;
|
|
66
|
+
}
|
|
67
|
+
function formatDateKey(now) {
|
|
68
|
+
return now.toISOString().slice(0, 10);
|
|
69
|
+
}
|
|
70
|
+
function formatErrorMessage(error) {
|
|
71
|
+
return error instanceof Error ? error.message : String(error);
|
|
72
|
+
}
|
|
73
|
+
function maybeParseJson(text) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(text);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
const match = text.match(/\{[\s\S]*\}/);
|
|
79
|
+
if (!match) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(match[0]);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function ensureArray(value) {
|
|
91
|
+
return Array.isArray(value) ? value : [];
|
|
92
|
+
}
|
|
93
|
+
class TelegramMemory {
|
|
94
|
+
constructor(config, deps = {}) {
|
|
95
|
+
assertValidTelegramMemoryConfig(config);
|
|
96
|
+
this.rawConfig = normalizeTelegramMemoryConfig(config);
|
|
97
|
+
this.provider = this.rawConfig.provider ?? 'openai';
|
|
98
|
+
this.logger = deps.logger;
|
|
99
|
+
this.now = deps.now ?? (() => new Date());
|
|
100
|
+
this.rootDir = node_path_1.default.resolve(this.rawConfig.storage?.rootDir ?? DEFAULT_STORAGE_DIR);
|
|
101
|
+
this.store = deps.store ?? new file_memory_1.TelegramFileMemoryStore(this.rootDir);
|
|
102
|
+
this.llm =
|
|
103
|
+
deps.llm ??
|
|
104
|
+
(this.provider === 'anthropic'
|
|
105
|
+
? new AnthropicProvider_1.AnthropicProvider({
|
|
106
|
+
apiKey: this.rawConfig.anthropic.apiKey,
|
|
107
|
+
model: this.rawConfig.anthropic?.model,
|
|
108
|
+
baseURL: this.rawConfig.anthropic?.baseURL,
|
|
109
|
+
headers: this.rawConfig.anthropic?.headers,
|
|
110
|
+
logger: deps.logger,
|
|
111
|
+
})
|
|
112
|
+
: new OpenAIProvider_1.OpenAIProvider({
|
|
113
|
+
apiKey: this.rawConfig.openai.apiKey,
|
|
114
|
+
model: this.rawConfig.openai?.model,
|
|
115
|
+
baseURL: this.rawConfig.openai?.baseURL,
|
|
116
|
+
headers: this.rawConfig.openai?.headers,
|
|
117
|
+
logger: deps.logger,
|
|
118
|
+
}));
|
|
119
|
+
this.embedder =
|
|
120
|
+
deps.embedder ??
|
|
121
|
+
(this.rawConfig.openai?.embeddingModel
|
|
122
|
+
? new local_index_1.OpenAIEmbeddingProvider({
|
|
123
|
+
apiKey: this.rawConfig.openai.apiKey,
|
|
124
|
+
baseURL: this.rawConfig.openai?.baseURL,
|
|
125
|
+
model: this.rawConfig.openai?.embeddingModel,
|
|
126
|
+
headers: this.rawConfig.openai?.headers,
|
|
127
|
+
})
|
|
128
|
+
: new local_index_1.LocalEmbeddingProvider());
|
|
129
|
+
this.recentDays = Math.max(1, this.rawConfig.behavior?.recentDays ?? DEFAULT_RECENT_DAYS);
|
|
130
|
+
this.maxProfileFacts = Math.max(1, this.rawConfig.behavior?.maxProfileFacts ?? DEFAULT_MAX_PROFILE_FACTS);
|
|
131
|
+
this.maxFactsPerMessage = Math.max(1, this.rawConfig.behavior?.maxFactsPerMessage ?? DEFAULT_MAX_FACTS_PER_MESSAGE);
|
|
132
|
+
this.shareProfileAcrossGroups =
|
|
133
|
+
this.rawConfig.behavior?.shareProfileAcrossGroups ?? DEFAULT_SHARE_PROFILE_ACROSS_GROUPS;
|
|
134
|
+
this.whatToRemember = this.rawConfig.whatToRemember?.trim() || undefined;
|
|
135
|
+
}
|
|
136
|
+
async rememberTelegramMessage(message, actor) {
|
|
137
|
+
await this.store.ensureReady();
|
|
138
|
+
const extraction = await this.extractFacts(message, actor);
|
|
139
|
+
const profileFacts = ensureArray(extraction.profile_facts).slice(0, this.maxFactsPerMessage);
|
|
140
|
+
const dailyFacts = ensureArray(extraction.daily_facts).slice(0, this.maxFactsPerMessage);
|
|
141
|
+
const shouldStore = extraction.should_store !== false && (profileFacts.length > 0 || dailyFacts.length > 0);
|
|
142
|
+
if (!shouldStore) {
|
|
143
|
+
return {
|
|
144
|
+
profileFacts: [],
|
|
145
|
+
dailyFacts: [],
|
|
146
|
+
skipped: true,
|
|
147
|
+
summary: {
|
|
148
|
+
added: 0,
|
|
149
|
+
profile: 0,
|
|
150
|
+
daily: 0,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const now = this.now();
|
|
155
|
+
const isoNow = now.toISOString();
|
|
156
|
+
const dateKey = formatDateKey(now);
|
|
157
|
+
const profile = await this.readWritableProfile(actor);
|
|
158
|
+
const effectiveProfileFacts = await this.collectProfileFacts(actor.groupId, actor.userId);
|
|
159
|
+
const baseProfile = {
|
|
160
|
+
...profile,
|
|
161
|
+
facts: this.shareProfileAcrossGroups ? effectiveProfileFacts : profile.facts,
|
|
162
|
+
};
|
|
163
|
+
baseProfile.actor.username = actor.username ?? baseProfile.actor.username;
|
|
164
|
+
baseProfile.actor.groupId = actor.groupId;
|
|
165
|
+
const mergedProfile = this.mergeProfileFacts(baseProfile, effectiveProfileFacts, profileFacts, actor, message, isoNow);
|
|
166
|
+
baseProfile.facts = mergedProfile.nextFacts;
|
|
167
|
+
baseProfile.updatedAt = isoNow;
|
|
168
|
+
await this.writeWritableProfile(baseProfile);
|
|
169
|
+
const daily = await this.store.readDailyLog(actor.groupId, actor.userId, dateKey);
|
|
170
|
+
daily.actor.username = actor.username ?? daily.actor.username;
|
|
171
|
+
const mappedDailyFacts = dailyFacts.map((fact) => this.toStoredFact(fact, {
|
|
172
|
+
actor,
|
|
173
|
+
sourceText: message,
|
|
174
|
+
nowIso: isoNow,
|
|
175
|
+
kind: 'daily',
|
|
176
|
+
path: this.store.toRelativePath(node_path_1.default.join(this.rootDir, 'groups', actor.groupId, 'members', actor.userId, 'memory', `${dateKey}.md`)),
|
|
177
|
+
}));
|
|
178
|
+
daily.entries.push(...mappedDailyFacts);
|
|
179
|
+
daily.updatedAt = isoNow;
|
|
180
|
+
await this.store.writeDailyLog(daily);
|
|
181
|
+
await this.refreshLocalIndexSafely(actor.groupId, actor.userId);
|
|
182
|
+
this.logger?.info?.('TelegramMemory stored extracted facts.', {
|
|
183
|
+
userId: actor.userId,
|
|
184
|
+
groupId: actor.groupId,
|
|
185
|
+
profileFacts: mergedProfile.nextFacts.length,
|
|
186
|
+
addedProfileFacts: mergedProfile.addedFacts.length,
|
|
187
|
+
addedDailyFacts: mappedDailyFacts.length,
|
|
188
|
+
});
|
|
189
|
+
return {
|
|
190
|
+
profileFacts: mergedProfile.addedFacts,
|
|
191
|
+
dailyFacts: mappedDailyFacts,
|
|
192
|
+
skipped: false,
|
|
193
|
+
summary: {
|
|
194
|
+
added: mergedProfile.addedFacts.length + mappedDailyFacts.length,
|
|
195
|
+
profile: mergedProfile.addedFacts.length,
|
|
196
|
+
daily: mappedDailyFacts.length,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async remember(message, actor) {
|
|
201
|
+
return this.rememberTelegramMessage(message, actor);
|
|
202
|
+
}
|
|
203
|
+
async recall(query, options) {
|
|
204
|
+
const facts = await this.searchIndexedFacts(query, options);
|
|
205
|
+
return facts.slice(0, Math.max(1, options.limit ?? 8)).map((fact) => ({
|
|
206
|
+
id: fact.id,
|
|
207
|
+
kind: fact.kind,
|
|
208
|
+
content: fact.content,
|
|
209
|
+
summary: fact.summary,
|
|
210
|
+
category: fact.category,
|
|
211
|
+
importance: fact.importance,
|
|
212
|
+
score: fact.score,
|
|
213
|
+
path: fact.path,
|
|
214
|
+
updatedAt: fact.updatedAt,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
async buildPromptContext(query, options) {
|
|
218
|
+
const stableProfileFacts = this.store
|
|
219
|
+
.sortFactsByUpdatedAt(await this.collectProfileFacts(options.groupId, options.userId))
|
|
220
|
+
.sort((left, right) => right.importance - left.importance || scoreFact(right, query) - scoreFact(left, query))
|
|
221
|
+
.slice(0, DEFAULT_PROFILE_CONTEXT_FACTS);
|
|
222
|
+
const recentMemories = (await this.searchIndexedFacts(query, {
|
|
223
|
+
...options,
|
|
224
|
+
includeProfile: false,
|
|
225
|
+
limit: DEFAULT_DAILY_CONTEXT_FACTS,
|
|
226
|
+
}))
|
|
227
|
+
.filter((fact) => fact.kind === 'daily' && fact.score >= DAILY_RECALL_MIN_SCORE)
|
|
228
|
+
.slice(0, DEFAULT_DAILY_CONTEXT_FACTS);
|
|
229
|
+
const memories = [
|
|
230
|
+
...stableProfileFacts.map((fact) => ({
|
|
231
|
+
id: fact.id,
|
|
232
|
+
kind: fact.kind,
|
|
233
|
+
content: fact.content,
|
|
234
|
+
summary: fact.summary,
|
|
235
|
+
category: fact.category,
|
|
236
|
+
importance: fact.importance,
|
|
237
|
+
score: scoreFact(fact, query),
|
|
238
|
+
path: fact.path,
|
|
239
|
+
updatedAt: fact.updatedAt,
|
|
240
|
+
})),
|
|
241
|
+
...recentMemories,
|
|
242
|
+
];
|
|
243
|
+
const lines = [
|
|
244
|
+
`Telegram memory context for user ${options.userId} in group ${options.groupId}.`,
|
|
245
|
+
`Current message: ${query}`,
|
|
246
|
+
'',
|
|
247
|
+
];
|
|
248
|
+
if (stableProfileFacts.length === 0) {
|
|
249
|
+
lines.push('Member profile:');
|
|
250
|
+
lines.push('- No saved profile yet.');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
lines.push('Member profile:');
|
|
254
|
+
for (const fact of stableProfileFacts) {
|
|
255
|
+
lines.push(`- ${fact.summary} | category=${fact.category} | importance=${fact.importance.toFixed(2)}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
if (recentMemories.length === 0) {
|
|
260
|
+
lines.push('Relevant recent memory:');
|
|
261
|
+
lines.push('- No relevant recent memory found.');
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
lines.push('Relevant recent memory:');
|
|
265
|
+
for (const memory of recentMemories) {
|
|
266
|
+
lines.push(`- ${memory.summary} | date=${memory.updatedAt.slice(0, 10)} | category=${memory.category} | score=${memory.score.toFixed(2)}`);
|
|
267
|
+
lines.push(` ${memory.content}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
text: `${lines.join('\n')}\n`,
|
|
272
|
+
memories,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async context(query, options) {
|
|
276
|
+
return this.buildPromptContext(query, options);
|
|
277
|
+
}
|
|
278
|
+
async getUserMemoryStats(userId, groupId) {
|
|
279
|
+
const stats = await this.store.getUserStats(groupId, userId);
|
|
280
|
+
const profileFacts = await this.collectProfileFacts(groupId, userId);
|
|
281
|
+
const profileTimestamps = profileFacts.map((fact) => fact.updatedAt);
|
|
282
|
+
const timestamps = [...profileTimestamps, stats.lastUpdatedAt]
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
.sort();
|
|
285
|
+
return {
|
|
286
|
+
profileFacts: profileFacts.length,
|
|
287
|
+
dailyFacts: stats.dailyFacts,
|
|
288
|
+
totalFacts: profileFacts.length + stats.dailyFacts,
|
|
289
|
+
lastUpdatedAt: timestamps.at(-1),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
async getMemory(memoryId) {
|
|
293
|
+
return this.store.getFactById(memoryId);
|
|
294
|
+
}
|
|
295
|
+
async updateMemory(memoryId, updates) {
|
|
296
|
+
const existing = await this.store.getFactById(memoryId);
|
|
297
|
+
if (!existing) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const actor = {
|
|
301
|
+
userId: existing.userId ?? '',
|
|
302
|
+
groupId: existing.groupId,
|
|
303
|
+
};
|
|
304
|
+
if (existing.kind === 'profile' && existing.userId) {
|
|
305
|
+
const profile = await this.readProfileForFact(existing);
|
|
306
|
+
const index = profile.facts.findIndex((fact) => fact.id === memoryId);
|
|
307
|
+
if (index === -1) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const updated = {
|
|
311
|
+
...profile.facts[index],
|
|
312
|
+
content: updates.content ?? profile.facts[index].content,
|
|
313
|
+
summary: updates.summary ?? profile.facts[index].summary,
|
|
314
|
+
importance: clampImportance(updates.importance ?? profile.facts[index].importance),
|
|
315
|
+
tags: updates.tags ? normalizeTags(updates.tags) : profile.facts[index].tags,
|
|
316
|
+
updatedAt: this.now().toISOString(),
|
|
317
|
+
};
|
|
318
|
+
profile.facts[index] = updated;
|
|
319
|
+
profile.updatedAt = updated.updatedAt;
|
|
320
|
+
await this.writeProfileForFact(profile, existing);
|
|
321
|
+
await this.refreshLocalIndexSafely(existing.groupId, existing.userId);
|
|
322
|
+
return updated;
|
|
323
|
+
}
|
|
324
|
+
if (existing.kind === 'daily' && existing.userId) {
|
|
325
|
+
const dateKey = node_path_1.default.basename(existing.path).replace(/\.md$/, '');
|
|
326
|
+
const daily = await this.store.readDailyLog(actor.groupId, actor.userId, dateKey);
|
|
327
|
+
const index = daily.entries.findIndex((fact) => fact.id === memoryId);
|
|
328
|
+
if (index === -1) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
const updated = {
|
|
332
|
+
...daily.entries[index],
|
|
333
|
+
content: updates.content ?? daily.entries[index].content,
|
|
334
|
+
summary: updates.summary ?? daily.entries[index].summary,
|
|
335
|
+
importance: clampImportance(updates.importance ?? daily.entries[index].importance),
|
|
336
|
+
tags: updates.tags ? normalizeTags(updates.tags) : daily.entries[index].tags,
|
|
337
|
+
updatedAt: this.now().toISOString(),
|
|
338
|
+
};
|
|
339
|
+
daily.entries[index] = updated;
|
|
340
|
+
daily.updatedAt = updated.updatedAt;
|
|
341
|
+
await this.store.writeDailyLog(daily);
|
|
342
|
+
await this.refreshLocalIndexSafely(existing.groupId, existing.userId);
|
|
343
|
+
return updated;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
async deleteMemory(memoryId) {
|
|
348
|
+
const existing = await this.store.getFactById(memoryId);
|
|
349
|
+
if (!existing?.userId) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (existing.kind === 'profile') {
|
|
353
|
+
const profile = await this.readProfileForFact(existing);
|
|
354
|
+
const nextFacts = profile.facts.filter((fact) => fact.id !== memoryId);
|
|
355
|
+
if (nextFacts.length === profile.facts.length) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
profile.facts = nextFacts;
|
|
359
|
+
profile.updatedAt = this.now().toISOString();
|
|
360
|
+
await this.writeProfileForFact(profile, existing);
|
|
361
|
+
await this.refreshLocalIndexSafely(existing.groupId, existing.userId);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
const dateKey = node_path_1.default.basename(existing.path).replace(/\.md$/, '');
|
|
365
|
+
const daily = await this.store.readDailyLog(existing.groupId, existing.userId, dateKey);
|
|
366
|
+
const nextEntries = daily.entries.filter((fact) => fact.id !== memoryId);
|
|
367
|
+
if (nextEntries.length === daily.entries.length) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
daily.entries = nextEntries;
|
|
371
|
+
daily.updatedAt = this.now().toISOString();
|
|
372
|
+
await this.store.writeDailyLog(daily);
|
|
373
|
+
await this.refreshLocalIndexSafely(existing.groupId, existing.userId);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
async getMemoryHistory(memoryId) {
|
|
377
|
+
const fact = await this.getMemory(memoryId);
|
|
378
|
+
return fact ? [fact] : [];
|
|
379
|
+
}
|
|
380
|
+
async checkHealth() {
|
|
381
|
+
const checks = [];
|
|
382
|
+
try {
|
|
383
|
+
await this.store.ensureReady();
|
|
384
|
+
const probeFile = node_path_1.default.join(this.rootDir, '.healthcheck');
|
|
385
|
+
await promises_1.default.writeFile(probeFile, 'ok', 'utf8');
|
|
386
|
+
await promises_1.default.unlink(probeFile);
|
|
387
|
+
checks.push({
|
|
388
|
+
name: 'storage',
|
|
389
|
+
ok: true,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
checks.push({
|
|
394
|
+
name: 'storage',
|
|
395
|
+
ok: false,
|
|
396
|
+
message: formatErrorMessage(error),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const apiKey = this.provider === 'anthropic'
|
|
400
|
+
? (this.rawConfig.anthropic?.apiKey?.trim() ?? '')
|
|
401
|
+
: (this.rawConfig.openai?.apiKey?.trim() ?? '');
|
|
402
|
+
checks.push({
|
|
403
|
+
name: 'llm',
|
|
404
|
+
ok: Boolean(apiKey),
|
|
405
|
+
message: apiKey ? undefined : 'LLM provider API key is missing.',
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
ok: checks.every((check) => check.ok),
|
|
409
|
+
checks,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
async preflight() {
|
|
413
|
+
const checks = [
|
|
414
|
+
{
|
|
415
|
+
name: 'config',
|
|
416
|
+
ok: true,
|
|
417
|
+
fatal: true,
|
|
418
|
+
message: 'TelegramMemory configuration is present.',
|
|
419
|
+
details: {
|
|
420
|
+
rootDir: this.rootDir,
|
|
421
|
+
recentDays: this.recentDays,
|
|
422
|
+
maxProfileFacts: this.maxProfileFacts,
|
|
423
|
+
},
|
|
424
|
+
},
|
|
425
|
+
];
|
|
426
|
+
const health = await this.checkHealth();
|
|
427
|
+
const storage = health.checks.find((check) => check.name === 'storage');
|
|
428
|
+
const llm = health.checks.find((check) => check.name === 'llm');
|
|
429
|
+
checks.push({
|
|
430
|
+
name: 'storage',
|
|
431
|
+
ok: storage?.ok ?? false,
|
|
432
|
+
fatal: true,
|
|
433
|
+
message: storage?.message ?? 'Storage is writable.',
|
|
434
|
+
});
|
|
435
|
+
checks.push({
|
|
436
|
+
name: 'llm',
|
|
437
|
+
ok: llm?.ok ?? false,
|
|
438
|
+
fatal: true,
|
|
439
|
+
message: llm?.message ?? 'LLM provider configuration is available.',
|
|
440
|
+
});
|
|
441
|
+
const report = {
|
|
442
|
+
ok: checks.every((check) => check.ok || !check.fatal),
|
|
443
|
+
checks,
|
|
444
|
+
};
|
|
445
|
+
this.logger?.info?.('TelegramMemory preflight completed.', {
|
|
446
|
+
ok: report.ok,
|
|
447
|
+
checks: report.checks.map((check) => ({
|
|
448
|
+
name: check.name,
|
|
449
|
+
ok: check.ok,
|
|
450
|
+
fatal: check.fatal,
|
|
451
|
+
})),
|
|
452
|
+
});
|
|
453
|
+
return report;
|
|
454
|
+
}
|
|
455
|
+
async extractFacts(message, actor) {
|
|
456
|
+
const prompt = [
|
|
457
|
+
'You are extracting durable Telegram member memory.',
|
|
458
|
+
'Return strict JSON.',
|
|
459
|
+
'Only keep information worth remembering for future group interactions.',
|
|
460
|
+
'Schema:',
|
|
461
|
+
'{',
|
|
462
|
+
' "should_store": boolean,',
|
|
463
|
+
' "profile_facts": [{"kind":"profile","summary":"...","content":"...","category":"...","tags":["..."],"importance":0.0,"confidence":0.0}],',
|
|
464
|
+
' "daily_facts": [{"kind":"daily","summary":"...","content":"...","category":"...","tags":["..."],"importance":0.0,"confidence":0.0}]',
|
|
465
|
+
'}',
|
|
466
|
+
'Profile facts are stable preferences, identity, habits, long-term relationships, ongoing work, recurring patterns.',
|
|
467
|
+
'Daily facts are recent events, plans, updates, one-off situations.',
|
|
468
|
+
'Do not include trivia, greetings, filler, or raw transcript copies.',
|
|
469
|
+
this.whatToRemember
|
|
470
|
+
? `Additional memory policy from the user:\n${this.whatToRemember}`
|
|
471
|
+
: 'Additional memory policy from the user:\nUse the default policy only.',
|
|
472
|
+
`groupId: ${actor.groupId}`,
|
|
473
|
+
`userId: ${actor.userId}`,
|
|
474
|
+
`username: ${actor.username ?? ''}`,
|
|
475
|
+
`message: ${JSON.stringify(message)}`,
|
|
476
|
+
].join('\n');
|
|
477
|
+
const raw = await this.llm.generate(prompt, {
|
|
478
|
+
temperature: 0.1,
|
|
479
|
+
maxTokens: 1200,
|
|
480
|
+
responseFormat: 'json_object',
|
|
481
|
+
});
|
|
482
|
+
const parsed = maybeParseJson(raw);
|
|
483
|
+
if (!parsed) {
|
|
484
|
+
this.logger?.warn?.('TelegramMemory extraction returned invalid JSON.', {
|
|
485
|
+
raw,
|
|
486
|
+
userId: actor.userId,
|
|
487
|
+
groupId: actor.groupId,
|
|
488
|
+
});
|
|
489
|
+
return { should_store: false, profile_facts: [], daily_facts: [] };
|
|
490
|
+
}
|
|
491
|
+
return parsed;
|
|
492
|
+
}
|
|
493
|
+
mergeProfileFacts(profile, effectiveProfileFacts, incomingFacts, actor, sourceText, nowIso) {
|
|
494
|
+
const bySummary = new Map();
|
|
495
|
+
const effectiveBySummary = new Map();
|
|
496
|
+
const addedFacts = [];
|
|
497
|
+
for (const fact of effectiveProfileFacts) {
|
|
498
|
+
effectiveBySummary.set(fact.summary.trim().toLowerCase(), fact);
|
|
499
|
+
}
|
|
500
|
+
for (const fact of profile.facts) {
|
|
501
|
+
bySummary.set(fact.summary.trim().toLowerCase(), fact);
|
|
502
|
+
}
|
|
503
|
+
for (const fact of incomingFacts) {
|
|
504
|
+
const key = fact.summary.trim().toLowerCase();
|
|
505
|
+
const mapped = this.toStoredFact(fact, {
|
|
506
|
+
actor,
|
|
507
|
+
sourceText,
|
|
508
|
+
nowIso,
|
|
509
|
+
kind: 'profile',
|
|
510
|
+
path: this.profileMarkdownPathForActor(actor),
|
|
511
|
+
});
|
|
512
|
+
const existingInCurrentGroup = bySummary.get(key);
|
|
513
|
+
const existingEffective = effectiveBySummary.get(key);
|
|
514
|
+
if (!existingInCurrentGroup && !existingEffective) {
|
|
515
|
+
bySummary.set(key, mapped);
|
|
516
|
+
addedFacts.push(mapped);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!existingInCurrentGroup) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
bySummary.set(key, {
|
|
523
|
+
...existingInCurrentGroup,
|
|
524
|
+
content: mapped.content,
|
|
525
|
+
summary: mapped.summary,
|
|
526
|
+
category: mapped.category,
|
|
527
|
+
tags: normalizeTags([...existingInCurrentGroup.tags, ...mapped.tags]),
|
|
528
|
+
importance: Math.max(existingInCurrentGroup.importance, mapped.importance),
|
|
529
|
+
confidence: Math.max(existingInCurrentGroup.confidence, mapped.confidence),
|
|
530
|
+
updatedAt: nowIso,
|
|
531
|
+
sourceText: mapped.sourceText,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
nextFacts: this.store
|
|
536
|
+
.sortFactsByUpdatedAt(Array.from(bySummary.values()))
|
|
537
|
+
.slice(0, this.maxProfileFacts),
|
|
538
|
+
addedFacts,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
toStoredFact(fact, params) {
|
|
542
|
+
return {
|
|
543
|
+
id: (0, node_crypto_1.randomUUID)(),
|
|
544
|
+
kind: params.kind,
|
|
545
|
+
scope: 'user',
|
|
546
|
+
userId: params.actor.userId,
|
|
547
|
+
groupId: params.actor.groupId,
|
|
548
|
+
content: fact.content.trim(),
|
|
549
|
+
summary: fact.summary.trim(),
|
|
550
|
+
category: normalizeCategory(fact.category),
|
|
551
|
+
tags: normalizeTags(fact.tags),
|
|
552
|
+
importance: clampImportance(fact.importance),
|
|
553
|
+
confidence: clampImportance(fact.confidence ?? 0.8),
|
|
554
|
+
sourceText: params.sourceText,
|
|
555
|
+
createdAt: params.nowIso,
|
|
556
|
+
updatedAt: params.nowIso,
|
|
557
|
+
path: params.path,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
async searchIndexedFacts(query, options) {
|
|
561
|
+
const minImportance = options.minImportance ?? 0;
|
|
562
|
+
const limit = Math.max(1, options.limit ?? 8);
|
|
563
|
+
const allFacts = await this.collectFactsForUser(options.groupId, options.userId, Math.max(1, options.recentDays ?? this.recentDays), options.includeProfile !== false);
|
|
564
|
+
if (allFacts.length === 0) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
const index = await this.ensureLocalIndex(options.groupId, options.userId);
|
|
568
|
+
const now = this.now();
|
|
569
|
+
const queryTokens = (0, local_index_1.tokenizeQuery)(query);
|
|
570
|
+
const queryEmbedding = await this.embedQuery(query);
|
|
571
|
+
const indexedById = new Map(index.entries.map((entry) => [entry.factId, entry]));
|
|
572
|
+
return allFacts
|
|
573
|
+
.filter((fact) => fact.importance >= minImportance)
|
|
574
|
+
.map((fact) => {
|
|
575
|
+
const indexed = indexedById.get(fact.id);
|
|
576
|
+
const score = indexed
|
|
577
|
+
? (0, local_index_1.scoreIndexedEntry)({
|
|
578
|
+
entry: indexed,
|
|
579
|
+
query,
|
|
580
|
+
queryTokens,
|
|
581
|
+
queryEmbedding,
|
|
582
|
+
now,
|
|
583
|
+
})
|
|
584
|
+
: scoreFact(fact, query);
|
|
585
|
+
return {
|
|
586
|
+
id: fact.id,
|
|
587
|
+
kind: fact.kind,
|
|
588
|
+
content: fact.content,
|
|
589
|
+
summary: fact.summary,
|
|
590
|
+
category: fact.category,
|
|
591
|
+
importance: fact.importance,
|
|
592
|
+
score,
|
|
593
|
+
path: fact.path,
|
|
594
|
+
updatedAt: fact.updatedAt,
|
|
595
|
+
};
|
|
596
|
+
})
|
|
597
|
+
.sort((left, right) => right.score - left.score ||
|
|
598
|
+
new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())
|
|
599
|
+
.slice(0, limit);
|
|
600
|
+
}
|
|
601
|
+
async collectFactsForUser(groupId, userId, recentDays, includeProfile) {
|
|
602
|
+
const dailies = await this.store.listRecentDailyLogs(groupId, userId, recentDays);
|
|
603
|
+
const facts = [];
|
|
604
|
+
if (includeProfile) {
|
|
605
|
+
facts.push(...(await this.collectProfileFacts(groupId, userId)));
|
|
606
|
+
}
|
|
607
|
+
for (const daily of dailies) {
|
|
608
|
+
facts.push(...daily.entries);
|
|
609
|
+
}
|
|
610
|
+
return facts;
|
|
611
|
+
}
|
|
612
|
+
async ensureLocalIndex(groupId, userId) {
|
|
613
|
+
const current = await this.store.readLocalIndex(groupId, userId);
|
|
614
|
+
const facts = await this.collectFactsForUser(groupId, userId, 3650, true);
|
|
615
|
+
const factFingerprint = facts
|
|
616
|
+
.map((fact) => `${fact.id}:${fact.updatedAt}`)
|
|
617
|
+
.sort()
|
|
618
|
+
.join('|');
|
|
619
|
+
const indexFingerprint = current.entries
|
|
620
|
+
.map((entry) => `${entry.factId}:${entry.updatedAt}`)
|
|
621
|
+
.sort()
|
|
622
|
+
.join('|');
|
|
623
|
+
if (current.model === this.embedder.model && factFingerprint === indexFingerprint) {
|
|
624
|
+
return current;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
return await this.rebuildLocalIndex(groupId, userId, current, facts);
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
this.logger?.warn?.('TelegramMemory local index rebuild failed. Falling back to lexical ranking.', {
|
|
631
|
+
error: formatErrorMessage(error),
|
|
632
|
+
userId,
|
|
633
|
+
groupId,
|
|
634
|
+
});
|
|
635
|
+
return current;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async rebuildLocalIndex(groupId, userId, existingIndex, knownFacts) {
|
|
639
|
+
const facts = knownFacts ?? (await this.collectFactsForUser(groupId, userId, 3650, true));
|
|
640
|
+
const previous = existingIndex ?? (await this.store.readLocalIndex(groupId, userId));
|
|
641
|
+
const reusable = new Map();
|
|
642
|
+
for (const entry of previous.entries) {
|
|
643
|
+
reusable.set(`${entry.factId}:${entry.updatedAt}:${previous.model}`, entry);
|
|
644
|
+
}
|
|
645
|
+
const entries = [];
|
|
646
|
+
const textsToEmbed = [];
|
|
647
|
+
const pendingFacts = [];
|
|
648
|
+
for (const fact of facts) {
|
|
649
|
+
const cacheKey = `${fact.id}:${fact.updatedAt}:${this.embedder.model}`;
|
|
650
|
+
const cached = reusable.get(cacheKey);
|
|
651
|
+
if (cached) {
|
|
652
|
+
entries.push(cached);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
pendingFacts.push(fact);
|
|
656
|
+
textsToEmbed.push((0, local_index_1.buildEmbeddingInput)(fact));
|
|
657
|
+
}
|
|
658
|
+
if (pendingFacts.length > 0) {
|
|
659
|
+
const embeddings = await this.embedTexts(textsToEmbed);
|
|
660
|
+
for (let index = 0; index < pendingFacts.length; index += 1) {
|
|
661
|
+
const fact = pendingFacts[index];
|
|
662
|
+
entries.push({
|
|
663
|
+
factId: fact.id,
|
|
664
|
+
kind: fact.kind,
|
|
665
|
+
path: fact.path,
|
|
666
|
+
updatedAt: fact.updatedAt,
|
|
667
|
+
summary: fact.summary,
|
|
668
|
+
content: fact.content,
|
|
669
|
+
category: fact.category,
|
|
670
|
+
tags: fact.tags,
|
|
671
|
+
importance: fact.importance,
|
|
672
|
+
embedding: embeddings[index] ?? [],
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const nextIndex = {
|
|
677
|
+
version: 1,
|
|
678
|
+
model: this.embedder.model,
|
|
679
|
+
updatedAt: this.now().toISOString(),
|
|
680
|
+
entries: entries.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
|
|
681
|
+
};
|
|
682
|
+
await this.store.writeLocalIndex(groupId, userId, nextIndex);
|
|
683
|
+
return nextIndex;
|
|
684
|
+
}
|
|
685
|
+
async embedQuery(query) {
|
|
686
|
+
try {
|
|
687
|
+
const [embedding] = await this.embedTexts([query]);
|
|
688
|
+
return embedding ?? null;
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
this.logger?.warn?.('TelegramMemory query embedding failed. Falling back to lexical ranking.', {
|
|
692
|
+
error: formatErrorMessage(error),
|
|
693
|
+
});
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
async embedTexts(texts) {
|
|
698
|
+
if (texts.length === 0) {
|
|
699
|
+
return [];
|
|
700
|
+
}
|
|
701
|
+
return this.embedder.embed(texts);
|
|
702
|
+
}
|
|
703
|
+
async refreshLocalIndexSafely(groupId, userId) {
|
|
704
|
+
try {
|
|
705
|
+
await this.rebuildLocalIndex(groupId, userId);
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
this.logger?.warn?.('TelegramMemory local index refresh failed after write.', {
|
|
709
|
+
error: formatErrorMessage(error),
|
|
710
|
+
userId,
|
|
711
|
+
groupId,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async collectProfileFacts(groupId, userId) {
|
|
716
|
+
if (!this.shareProfileAcrossGroups) {
|
|
717
|
+
const profile = await this.store.readProfile(groupId, userId);
|
|
718
|
+
return profile.facts;
|
|
719
|
+
}
|
|
720
|
+
const sharedProfile = await this.store.readSharedProfile(userId, groupId);
|
|
721
|
+
const profiles = [sharedProfile, ...(await this.store.listGroupProfilesForUser(userId))];
|
|
722
|
+
const bySummary = new Map();
|
|
723
|
+
const allFacts = profiles
|
|
724
|
+
.flatMap((profile) => profile.facts)
|
|
725
|
+
.sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
|
|
726
|
+
for (const fact of allFacts) {
|
|
727
|
+
const key = fact.summary.trim().toLowerCase();
|
|
728
|
+
const existing = bySummary.get(key);
|
|
729
|
+
if (!existing) {
|
|
730
|
+
bySummary.set(key, fact);
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const newer = new Date(fact.updatedAt).getTime() > new Date(existing.updatedAt).getTime()
|
|
734
|
+
? fact
|
|
735
|
+
: existing;
|
|
736
|
+
const older = newer === fact ? existing : fact;
|
|
737
|
+
bySummary.set(key, {
|
|
738
|
+
...newer,
|
|
739
|
+
tags: normalizeTags([...newer.tags, ...older.tags]),
|
|
740
|
+
importance: Math.max(newer.importance, older.importance),
|
|
741
|
+
confidence: Math.max(newer.confidence, older.confidence),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
return this.store.sortFactsByUpdatedAt(Array.from(bySummary.values()));
|
|
745
|
+
}
|
|
746
|
+
async readWritableProfile(actor) {
|
|
747
|
+
return this.shareProfileAcrossGroups
|
|
748
|
+
? this.store.readSharedProfile(actor.userId, actor.groupId)
|
|
749
|
+
: this.store.readProfile(actor.groupId, actor.userId);
|
|
750
|
+
}
|
|
751
|
+
async writeWritableProfile(profile) {
|
|
752
|
+
if (this.shareProfileAcrossGroups) {
|
|
753
|
+
await this.store.writeSharedProfile(profile);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
await this.store.writeProfile(profile);
|
|
757
|
+
}
|
|
758
|
+
async readProfileForFact(fact) {
|
|
759
|
+
if (!fact.userId) {
|
|
760
|
+
throw new Error('Cannot read a profile fact without userId.');
|
|
761
|
+
}
|
|
762
|
+
return this.isSharedProfilePath(fact.path)
|
|
763
|
+
? this.store.readSharedProfile(fact.userId, fact.groupId)
|
|
764
|
+
: this.store.readProfile(fact.groupId, fact.userId);
|
|
765
|
+
}
|
|
766
|
+
async writeProfileForFact(profile, fact) {
|
|
767
|
+
if (this.isSharedProfilePath(fact.path)) {
|
|
768
|
+
await this.store.writeSharedProfile(profile);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
await this.store.writeProfile(profile);
|
|
772
|
+
}
|
|
773
|
+
isSharedProfilePath(relativePath) {
|
|
774
|
+
return relativePath.split('/')[0] === 'users';
|
|
775
|
+
}
|
|
776
|
+
profileMarkdownPathForActor(actor) {
|
|
777
|
+
const absolutePath = this.shareProfileAcrossGroups
|
|
778
|
+
? node_path_1.default.join(this.rootDir, 'users', actor.userId, 'PROFILE.md')
|
|
779
|
+
: node_path_1.default.join(this.rootDir, 'groups', actor.groupId, 'members', actor.userId, 'PROFILE.md');
|
|
780
|
+
return this.store.toRelativePath(absolutePath);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
exports.TelegramMemory = TelegramMemory;
|
|
784
|
+
async function createTelegramMemory(config, dependencies = {}) {
|
|
785
|
+
const normalized = normalizeTelegramMemoryConfig(config);
|
|
786
|
+
assertValidTelegramMemoryConfig(normalized);
|
|
787
|
+
const memory = new TelegramMemory(normalized, dependencies);
|
|
788
|
+
const report = await memory.preflight();
|
|
789
|
+
if (!report.ok) {
|
|
790
|
+
const fatalChecks = report.checks
|
|
791
|
+
.filter((check) => check.fatal && !check.ok)
|
|
792
|
+
.map((check) => `${check.name}: ${check.message ?? 'failed'}`);
|
|
793
|
+
throw new Error(`TelegramMemory preflight failed. ${fatalChecks.join('; ')}`);
|
|
794
|
+
}
|
|
795
|
+
return memory;
|
|
796
|
+
}
|
|
797
|
+
function validateTelegramMemoryConfig(config) {
|
|
798
|
+
const normalized = normalizeTelegramMemoryConfig(config);
|
|
799
|
+
const issues = [];
|
|
800
|
+
if (normalized.provider === 'anthropic') {
|
|
801
|
+
if (!normalized.anthropic?.apiKey?.trim()) {
|
|
802
|
+
issues.push({
|
|
803
|
+
field: 'anthropic.apiKey',
|
|
804
|
+
message: 'Anthropic API key is required when provider=anthropic.',
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
else if (!normalized.openai?.apiKey?.trim()) {
|
|
809
|
+
issues.push({
|
|
810
|
+
field: 'apiKey',
|
|
811
|
+
message: 'OpenAI-compatible API key is required when provider=openai.',
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (normalized.storage?.rootDir !== undefined && !normalized.storage.rootDir.trim()) {
|
|
815
|
+
issues.push({
|
|
816
|
+
field: 'dir',
|
|
817
|
+
message: 'dir cannot be empty.',
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
if (normalized.behavior?.recentDays !== undefined &&
|
|
821
|
+
(!Number.isFinite(normalized.behavior.recentDays) || normalized.behavior.recentDays <= 0)) {
|
|
822
|
+
issues.push({
|
|
823
|
+
field: 'behavior.recentDays',
|
|
824
|
+
message: 'behavior.recentDays must be greater than 0.',
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
if (normalized.behavior?.maxProfileFacts !== undefined &&
|
|
828
|
+
(!Number.isFinite(normalized.behavior.maxProfileFacts) ||
|
|
829
|
+
normalized.behavior.maxProfileFacts <= 0)) {
|
|
830
|
+
issues.push({
|
|
831
|
+
field: 'behavior.maxProfileFacts',
|
|
832
|
+
message: 'behavior.maxProfileFacts must be greater than 0.',
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (normalized.behavior?.maxFactsPerMessage !== undefined &&
|
|
836
|
+
(!Number.isFinite(normalized.behavior.maxFactsPerMessage) ||
|
|
837
|
+
normalized.behavior.maxFactsPerMessage <= 0)) {
|
|
838
|
+
issues.push({
|
|
839
|
+
field: 'behavior.maxFactsPerMessage',
|
|
840
|
+
message: 'behavior.maxFactsPerMessage must be greater than 0.',
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
return issues;
|
|
844
|
+
}
|
|
845
|
+
function assertValidTelegramMemoryConfig(config) {
|
|
846
|
+
const issues = validateTelegramMemoryConfig(config);
|
|
847
|
+
if (issues.length === 0) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
throw new TelegramMemoryConfigError([
|
|
851
|
+
'Invalid TelegramMemory configuration:',
|
|
852
|
+
...issues.map((issue) => `- ${issue.field}: ${issue.message}`),
|
|
853
|
+
].join('\n'), issues);
|
|
854
|
+
}
|
|
855
|
+
function telegramMemoryConfigFromEnv(options = {}) {
|
|
856
|
+
const env = options.env ?? process.env;
|
|
857
|
+
const openAiHeaders = parseOptionalJsonObject(env.OPENAI_HEADERS_JSON);
|
|
858
|
+
const providerHeaders = {
|
|
859
|
+
...openAiHeaders,
|
|
860
|
+
...(env.OPENROUTER_HTTP_REFERER ? { 'HTTP-Referer': env.OPENROUTER_HTTP_REFERER } : {}),
|
|
861
|
+
...(env.OPENROUTER_APP_NAME ? { 'X-Title': env.OPENROUTER_APP_NAME } : {}),
|
|
862
|
+
};
|
|
863
|
+
const config = {
|
|
864
|
+
provider: parseProvider(env.TELEGRAM_MEMORY_LLM_PROVIDER),
|
|
865
|
+
apiKey: env.OPENAI_API_KEY ?? '',
|
|
866
|
+
model: env.OPENAI_MODEL,
|
|
867
|
+
baseURL: env.OPENAI_BASE_URL,
|
|
868
|
+
embeddingModel: env.OPENAI_EMBEDDING_MODEL,
|
|
869
|
+
headers: Object.keys(providerHeaders).length > 0 ? providerHeaders : undefined,
|
|
870
|
+
dir: env.TELEGRAM_MEMORY_DIR || undefined,
|
|
871
|
+
whatToRemember: env.TELEGRAM_MEMORY_POLICY || undefined,
|
|
872
|
+
anthropic: {
|
|
873
|
+
apiKey: env.ANTHROPIC_API_KEY ?? '',
|
|
874
|
+
model: env.ANTHROPIC_MODEL,
|
|
875
|
+
baseURL: env.ANTHROPIC_BASE_URL,
|
|
876
|
+
headers: parseOptionalJsonObject(env.ANTHROPIC_HEADERS_JSON),
|
|
877
|
+
},
|
|
878
|
+
behavior: {
|
|
879
|
+
recentDays: parseOptionalInteger(env.TELEGRAM_MEMORY_RECENT_DAYS),
|
|
880
|
+
maxProfileFacts: parseOptionalInteger(env.TELEGRAM_MEMORY_MAX_PROFILE_FACTS),
|
|
881
|
+
maxFactsPerMessage: parseOptionalInteger(env.TELEGRAM_MEMORY_MAX_FACTS_PER_MESSAGE),
|
|
882
|
+
shareProfileAcrossGroups: parseOptionalBoolean(env.TELEGRAM_MEMORY_SHARE_PROFILE_ACROSS_GROUPS),
|
|
883
|
+
},
|
|
884
|
+
};
|
|
885
|
+
return normalizeTelegramMemoryConfig(config);
|
|
886
|
+
}
|
|
887
|
+
async function createTelegramMemoryFromEnv(options = {}, dependencies = {}) {
|
|
888
|
+
return createTelegramMemory(telegramMemoryConfigFromEnv(options), dependencies);
|
|
889
|
+
}
|
|
890
|
+
function parseOptionalInteger(value) {
|
|
891
|
+
if (!value?.trim()) {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
const parsed = Number.parseInt(value, 10);
|
|
895
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
896
|
+
}
|
|
897
|
+
function parseProvider(value) {
|
|
898
|
+
if (!value?.trim()) {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
const normalized = value.trim().toLowerCase();
|
|
902
|
+
if (normalized === 'openai') {
|
|
903
|
+
return 'openai';
|
|
904
|
+
}
|
|
905
|
+
if (normalized === 'anthropic') {
|
|
906
|
+
return 'anthropic';
|
|
907
|
+
}
|
|
908
|
+
return undefined;
|
|
909
|
+
}
|
|
910
|
+
function parseOptionalBoolean(value) {
|
|
911
|
+
if (!value?.trim()) {
|
|
912
|
+
return undefined;
|
|
913
|
+
}
|
|
914
|
+
const normalized = value.trim().toLowerCase();
|
|
915
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
|
916
|
+
return true;
|
|
917
|
+
}
|
|
918
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
return undefined;
|
|
922
|
+
}
|
|
923
|
+
function parseOptionalJsonObject(value) {
|
|
924
|
+
if (!value?.trim()) {
|
|
925
|
+
return undefined;
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
const parsed = JSON.parse(value);
|
|
929
|
+
const entries = Object.entries(parsed)
|
|
930
|
+
.filter(([, entryValue]) => typeof entryValue === 'string')
|
|
931
|
+
.map(([key, entryValue]) => [key, entryValue]);
|
|
932
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
return undefined;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
function normalizeTelegramMemoryConfig(config) {
|
|
939
|
+
const provider = config.provider ?? (config.anthropic?.apiKey ? 'anthropic' : 'openai');
|
|
940
|
+
const apiKey = config.apiKey ?? config.openai?.apiKey ?? '';
|
|
941
|
+
const model = config.model ?? config.openai?.model;
|
|
942
|
+
const baseURL = config.baseURL ?? config.openai?.baseURL;
|
|
943
|
+
const embeddingModel = config.embeddingModel ?? config.openai?.embeddingModel;
|
|
944
|
+
const headers = config.headers ?? config.openai?.headers;
|
|
945
|
+
const dir = config.dir ?? config.storage?.rootDir;
|
|
946
|
+
const shareProfileAcrossGroups = config.behavior?.shareProfileAcrossGroups;
|
|
947
|
+
const anthropicApiKey = config.anthropic?.apiKey ?? '';
|
|
948
|
+
const anthropicModel = config.anthropic?.model;
|
|
949
|
+
const anthropicBaseURL = config.anthropic?.baseURL;
|
|
950
|
+
const anthropicHeaders = config.anthropic?.headers;
|
|
951
|
+
return {
|
|
952
|
+
...config,
|
|
953
|
+
provider,
|
|
954
|
+
apiKey,
|
|
955
|
+
model,
|
|
956
|
+
baseURL,
|
|
957
|
+
embeddingModel,
|
|
958
|
+
headers,
|
|
959
|
+
dir,
|
|
960
|
+
openai: {
|
|
961
|
+
apiKey,
|
|
962
|
+
model,
|
|
963
|
+
baseURL,
|
|
964
|
+
embeddingModel,
|
|
965
|
+
headers,
|
|
966
|
+
},
|
|
967
|
+
anthropic: {
|
|
968
|
+
apiKey: anthropicApiKey,
|
|
969
|
+
model: anthropicModel,
|
|
970
|
+
baseURL: anthropicBaseURL,
|
|
971
|
+
headers: anthropicHeaders,
|
|
972
|
+
},
|
|
973
|
+
storage: {
|
|
974
|
+
rootDir: dir,
|
|
975
|
+
},
|
|
976
|
+
behavior: {
|
|
977
|
+
...config.behavior,
|
|
978
|
+
shareProfileAcrossGroups,
|
|
979
|
+
},
|
|
980
|
+
};
|
|
981
|
+
}
|