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,72 @@
|
|
|
1
|
+
import type { StoredLocalIndex } from './local-index';
|
|
2
|
+
export interface StoredMemoryFact {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: 'profile' | 'daily';
|
|
5
|
+
scope: 'user' | 'group';
|
|
6
|
+
userId?: string;
|
|
7
|
+
groupId: string;
|
|
8
|
+
content: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
category: string;
|
|
11
|
+
tags: string[];
|
|
12
|
+
importance: number;
|
|
13
|
+
confidence: number;
|
|
14
|
+
sourceText: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
updatedAt: string;
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
export interface StoredProfile {
|
|
20
|
+
scope?: 'user' | 'group';
|
|
21
|
+
actor: {
|
|
22
|
+
userId: string;
|
|
23
|
+
groupId: string;
|
|
24
|
+
username?: string;
|
|
25
|
+
};
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
facts: StoredMemoryFact[];
|
|
28
|
+
}
|
|
29
|
+
export interface StoredDailyLog {
|
|
30
|
+
date: string;
|
|
31
|
+
actor: {
|
|
32
|
+
userId: string;
|
|
33
|
+
groupId: string;
|
|
34
|
+
username?: string;
|
|
35
|
+
};
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
entries: StoredMemoryFact[];
|
|
38
|
+
}
|
|
39
|
+
export declare class TelegramFileMemoryStore {
|
|
40
|
+
private readonly rootDir;
|
|
41
|
+
constructor(rootDir: string);
|
|
42
|
+
getRootDir(): string;
|
|
43
|
+
ensureReady(): Promise<void>;
|
|
44
|
+
private memberDir;
|
|
45
|
+
private userDir;
|
|
46
|
+
private profileJsonPath;
|
|
47
|
+
private profileMarkdownPath;
|
|
48
|
+
private sharedProfileJsonPath;
|
|
49
|
+
private sharedProfileMarkdownPath;
|
|
50
|
+
private dailyJsonPath;
|
|
51
|
+
private dailyMarkdownPath;
|
|
52
|
+
private localIndexPath;
|
|
53
|
+
readProfile(groupId: string, userId: string): Promise<StoredProfile>;
|
|
54
|
+
readSharedProfile(userId: string, groupId: string): Promise<StoredProfile>;
|
|
55
|
+
listGroupProfilesForUser(userId: string): Promise<StoredProfile[]>;
|
|
56
|
+
writeProfile(profile: StoredProfile): Promise<void>;
|
|
57
|
+
writeSharedProfile(profile: StoredProfile): Promise<void>;
|
|
58
|
+
readDailyLog(groupId: string, userId: string, dateKey: string): Promise<StoredDailyLog>;
|
|
59
|
+
writeDailyLog(daily: StoredDailyLog): Promise<void>;
|
|
60
|
+
readLocalIndex(groupId: string, userId: string): Promise<StoredLocalIndex>;
|
|
61
|
+
writeLocalIndex(groupId: string, userId: string, index: StoredLocalIndex): Promise<void>;
|
|
62
|
+
listRecentDailyLogs(groupId: string, userId: string, limit: number): Promise<StoredDailyLog[]>;
|
|
63
|
+
getFactById(memoryId: string): Promise<StoredMemoryFact | null>;
|
|
64
|
+
getUserStats(groupId: string, userId: string): Promise<{
|
|
65
|
+
profileFacts: number;
|
|
66
|
+
dailyFacts: number;
|
|
67
|
+
totalFacts: number;
|
|
68
|
+
lastUpdatedAt?: string;
|
|
69
|
+
}>;
|
|
70
|
+
toRelativePath(absolutePath: string): string;
|
|
71
|
+
sortFactsByUpdatedAt(facts: StoredMemoryFact[]): StoredMemoryFact[];
|
|
72
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
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.TelegramFileMemoryStore = void 0;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
function toPosixRelative(rootDir, absolutePath) {
|
|
10
|
+
return node_path_1.default.relative(rootDir, absolutePath).replace(/\\/g, '/');
|
|
11
|
+
}
|
|
12
|
+
async function readJsonFile(filePath, fallback) {
|
|
13
|
+
try {
|
|
14
|
+
const raw = await promises_1.default.readFile(filePath, 'utf8');
|
|
15
|
+
return JSON.parse(raw);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const code = error.code;
|
|
19
|
+
if (code === 'ENOENT') {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function writeTextFile(filePath, content) {
|
|
26
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
27
|
+
await promises_1.default.writeFile(filePath, content, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
function escapeMarkdown(value) {
|
|
30
|
+
return value.replace(/\r\n/g, '\n').trim();
|
|
31
|
+
}
|
|
32
|
+
function renderProfileMarkdown(profile) {
|
|
33
|
+
const lines = [
|
|
34
|
+
'# Member Profile',
|
|
35
|
+
'',
|
|
36
|
+
`- scope: ${profile.scope ?? 'group'}`,
|
|
37
|
+
`- userId: ${profile.actor.userId}`,
|
|
38
|
+
`- groupId: ${profile.actor.groupId}`,
|
|
39
|
+
];
|
|
40
|
+
if (profile.actor.username) {
|
|
41
|
+
lines.push(`- username: ${profile.actor.username}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(`- updatedAt: ${profile.updatedAt}`, '');
|
|
44
|
+
if (profile.facts.length === 0) {
|
|
45
|
+
lines.push('No long-term memory yet.', '');
|
|
46
|
+
return `${lines.join('\n')}\n`;
|
|
47
|
+
}
|
|
48
|
+
for (const fact of profile.facts) {
|
|
49
|
+
lines.push(`## ${escapeMarkdown(fact.summary)}`);
|
|
50
|
+
lines.push(`- id: ${fact.id}`);
|
|
51
|
+
lines.push(`- kind: ${fact.kind}`);
|
|
52
|
+
lines.push(`- category: ${fact.category}`);
|
|
53
|
+
lines.push(`- importance: ${fact.importance}`);
|
|
54
|
+
lines.push(`- confidence: ${fact.confidence}`);
|
|
55
|
+
if (fact.tags.length > 0) {
|
|
56
|
+
lines.push(`- tags: ${fact.tags.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
lines.push(`- updatedAt: ${fact.updatedAt}`);
|
|
59
|
+
lines.push('');
|
|
60
|
+
lines.push(escapeMarkdown(fact.content));
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
return `${lines.join('\n')}\n`;
|
|
64
|
+
}
|
|
65
|
+
function renderDailyMarkdown(daily) {
|
|
66
|
+
const lines = [
|
|
67
|
+
`# Daily Memory ${daily.date}`,
|
|
68
|
+
'',
|
|
69
|
+
`- userId: ${daily.actor.userId}`,
|
|
70
|
+
`- groupId: ${daily.actor.groupId}`,
|
|
71
|
+
];
|
|
72
|
+
if (daily.actor.username) {
|
|
73
|
+
lines.push(`- username: ${daily.actor.username}`);
|
|
74
|
+
}
|
|
75
|
+
lines.push(`- updatedAt: ${daily.updatedAt}`, '');
|
|
76
|
+
if (daily.entries.length === 0) {
|
|
77
|
+
lines.push('No daily memory yet.', '');
|
|
78
|
+
return `${lines.join('\n')}\n`;
|
|
79
|
+
}
|
|
80
|
+
for (const entry of daily.entries) {
|
|
81
|
+
lines.push(`## ${escapeMarkdown(entry.summary)}`);
|
|
82
|
+
lines.push(`- id: ${entry.id}`);
|
|
83
|
+
lines.push(`- category: ${entry.category}`);
|
|
84
|
+
lines.push(`- importance: ${entry.importance}`);
|
|
85
|
+
lines.push(`- confidence: ${entry.confidence}`);
|
|
86
|
+
if (entry.tags.length > 0) {
|
|
87
|
+
lines.push(`- tags: ${entry.tags.join(', ')}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push(`- createdAt: ${entry.createdAt}`);
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push(escapeMarkdown(entry.content));
|
|
92
|
+
lines.push('');
|
|
93
|
+
}
|
|
94
|
+
return `${lines.join('\n')}\n`;
|
|
95
|
+
}
|
|
96
|
+
function compareByUpdatedAt(a, b) {
|
|
97
|
+
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
|
98
|
+
}
|
|
99
|
+
class TelegramFileMemoryStore {
|
|
100
|
+
constructor(rootDir) {
|
|
101
|
+
this.rootDir = rootDir;
|
|
102
|
+
}
|
|
103
|
+
getRootDir() {
|
|
104
|
+
return this.rootDir;
|
|
105
|
+
}
|
|
106
|
+
async ensureReady() {
|
|
107
|
+
await promises_1.default.mkdir(this.rootDir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
memberDir(groupId, userId) {
|
|
110
|
+
return node_path_1.default.join(this.rootDir, 'groups', groupId, 'members', userId);
|
|
111
|
+
}
|
|
112
|
+
userDir(userId) {
|
|
113
|
+
return node_path_1.default.join(this.rootDir, 'users', userId);
|
|
114
|
+
}
|
|
115
|
+
profileJsonPath(groupId, userId) {
|
|
116
|
+
return node_path_1.default.join(this.memberDir(groupId, userId), 'PROFILE.json');
|
|
117
|
+
}
|
|
118
|
+
profileMarkdownPath(groupId, userId) {
|
|
119
|
+
return node_path_1.default.join(this.memberDir(groupId, userId), 'PROFILE.md');
|
|
120
|
+
}
|
|
121
|
+
sharedProfileJsonPath(userId) {
|
|
122
|
+
return node_path_1.default.join(this.userDir(userId), 'PROFILE.json');
|
|
123
|
+
}
|
|
124
|
+
sharedProfileMarkdownPath(userId) {
|
|
125
|
+
return node_path_1.default.join(this.userDir(userId), 'PROFILE.md');
|
|
126
|
+
}
|
|
127
|
+
dailyJsonPath(groupId, userId, dateKey) {
|
|
128
|
+
return node_path_1.default.join(this.memberDir(groupId, userId), 'memory', `${dateKey}.json`);
|
|
129
|
+
}
|
|
130
|
+
dailyMarkdownPath(groupId, userId, dateKey) {
|
|
131
|
+
return node_path_1.default.join(this.memberDir(groupId, userId), 'memory', `${dateKey}.md`);
|
|
132
|
+
}
|
|
133
|
+
localIndexPath(groupId, userId) {
|
|
134
|
+
return node_path_1.default.join(this.memberDir(groupId, userId), 'INDEX.json');
|
|
135
|
+
}
|
|
136
|
+
async readProfile(groupId, userId) {
|
|
137
|
+
return readJsonFile(this.profileJsonPath(groupId, userId), {
|
|
138
|
+
scope: 'group',
|
|
139
|
+
actor: { userId, groupId },
|
|
140
|
+
updatedAt: new Date(0).toISOString(),
|
|
141
|
+
facts: [],
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async readSharedProfile(userId, groupId) {
|
|
145
|
+
return readJsonFile(this.sharedProfileJsonPath(userId), {
|
|
146
|
+
scope: 'user',
|
|
147
|
+
actor: { userId, groupId },
|
|
148
|
+
updatedAt: new Date(0).toISOString(),
|
|
149
|
+
facts: [],
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async listGroupProfilesForUser(userId) {
|
|
153
|
+
const groupsDir = node_path_1.default.join(this.rootDir, 'groups');
|
|
154
|
+
try {
|
|
155
|
+
const entries = await promises_1.default.readdir(groupsDir, { withFileTypes: true, encoding: 'utf8' });
|
|
156
|
+
const profiles = [];
|
|
157
|
+
for (const entry of entries) {
|
|
158
|
+
if (!entry.isDirectory()) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const profilePath = this.profileJsonPath(entry.name, userId);
|
|
162
|
+
try {
|
|
163
|
+
const raw = await promises_1.default.readFile(profilePath, 'utf8');
|
|
164
|
+
profiles.push(JSON.parse(raw));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
const code = error.code;
|
|
168
|
+
if (code === 'ENOENT') {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return profiles;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const code = error.code;
|
|
178
|
+
if (code === 'ENOENT') {
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async writeProfile(profile) {
|
|
185
|
+
const nextProfile = {
|
|
186
|
+
...profile,
|
|
187
|
+
scope: 'group',
|
|
188
|
+
};
|
|
189
|
+
const filePath = this.profileJsonPath(profile.actor.groupId, profile.actor.userId);
|
|
190
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
191
|
+
await promises_1.default.writeFile(filePath, `${JSON.stringify(nextProfile, null, 2)}\n`, 'utf8');
|
|
192
|
+
await writeTextFile(this.profileMarkdownPath(profile.actor.groupId, profile.actor.userId), renderProfileMarkdown(nextProfile));
|
|
193
|
+
}
|
|
194
|
+
async writeSharedProfile(profile) {
|
|
195
|
+
const sharedPath = this.toRelativePath(this.sharedProfileMarkdownPath(profile.actor.userId));
|
|
196
|
+
const nextProfile = {
|
|
197
|
+
...profile,
|
|
198
|
+
scope: 'user',
|
|
199
|
+
facts: profile.facts.map((fact) => ({
|
|
200
|
+
...fact,
|
|
201
|
+
path: sharedPath,
|
|
202
|
+
})),
|
|
203
|
+
};
|
|
204
|
+
const filePath = this.sharedProfileJsonPath(profile.actor.userId);
|
|
205
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
206
|
+
await promises_1.default.writeFile(filePath, `${JSON.stringify(nextProfile, null, 2)}\n`, 'utf8');
|
|
207
|
+
await writeTextFile(this.sharedProfileMarkdownPath(profile.actor.userId), renderProfileMarkdown(nextProfile));
|
|
208
|
+
}
|
|
209
|
+
async readDailyLog(groupId, userId, dateKey) {
|
|
210
|
+
return readJsonFile(this.dailyJsonPath(groupId, userId, dateKey), {
|
|
211
|
+
date: dateKey,
|
|
212
|
+
actor: { userId, groupId },
|
|
213
|
+
updatedAt: new Date(0).toISOString(),
|
|
214
|
+
entries: [],
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async writeDailyLog(daily) {
|
|
218
|
+
const filePath = this.dailyJsonPath(daily.actor.groupId, daily.actor.userId, daily.date);
|
|
219
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
220
|
+
await promises_1.default.writeFile(filePath, `${JSON.stringify(daily, null, 2)}\n`, 'utf8');
|
|
221
|
+
await writeTextFile(this.dailyMarkdownPath(daily.actor.groupId, daily.actor.userId, daily.date), renderDailyMarkdown(daily));
|
|
222
|
+
}
|
|
223
|
+
async readLocalIndex(groupId, userId) {
|
|
224
|
+
return readJsonFile(this.localIndexPath(groupId, userId), {
|
|
225
|
+
version: 1,
|
|
226
|
+
model: '',
|
|
227
|
+
updatedAt: new Date(0).toISOString(),
|
|
228
|
+
entries: [],
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async writeLocalIndex(groupId, userId, index) {
|
|
232
|
+
const filePath = this.localIndexPath(groupId, userId);
|
|
233
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(filePath), { recursive: true });
|
|
234
|
+
await promises_1.default.writeFile(filePath, `${JSON.stringify(index, null, 2)}\n`, 'utf8');
|
|
235
|
+
}
|
|
236
|
+
async listRecentDailyLogs(groupId, userId, limit) {
|
|
237
|
+
const memoryDir = node_path_1.default.join(this.memberDir(groupId, userId), 'memory');
|
|
238
|
+
try {
|
|
239
|
+
const entries = await promises_1.default.readdir(memoryDir, { withFileTypes: true, encoding: 'utf8' });
|
|
240
|
+
const jsonFiles = entries
|
|
241
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
|
|
242
|
+
.map((entry) => entry.name)
|
|
243
|
+
.sort()
|
|
244
|
+
.reverse()
|
|
245
|
+
.slice(0, limit);
|
|
246
|
+
return Promise.all(jsonFiles.map((name) => readJsonFile(node_path_1.default.join(memoryDir, name), {
|
|
247
|
+
date: name.replace(/\.json$/, ''),
|
|
248
|
+
actor: { userId, groupId },
|
|
249
|
+
updatedAt: new Date(0).toISOString(),
|
|
250
|
+
entries: [],
|
|
251
|
+
})));
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const code = error.code;
|
|
255
|
+
if (code === 'ENOENT') {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async getFactById(memoryId) {
|
|
262
|
+
const search = async (dir) => {
|
|
263
|
+
try {
|
|
264
|
+
const entries = await promises_1.default.readdir(dir, { withFileTypes: true, encoding: 'utf8' });
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const fullPath = node_path_1.default.join(dir, entry.name);
|
|
267
|
+
if (entry.isDirectory()) {
|
|
268
|
+
const nested = await search(fullPath);
|
|
269
|
+
if (nested) {
|
|
270
|
+
return nested;
|
|
271
|
+
}
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (!entry.isFile() || !entry.name.endsWith('.json')) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const payload = await readJsonFile(fullPath, {});
|
|
278
|
+
const fact = payload.facts?.find((item) => item.id === memoryId) ??
|
|
279
|
+
payload.entries?.find((item) => item.id === memoryId);
|
|
280
|
+
if (fact) {
|
|
281
|
+
return fact;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
const code = error.code;
|
|
287
|
+
if (code === 'ENOENT') {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
};
|
|
294
|
+
for (const searchRoot of [
|
|
295
|
+
node_path_1.default.join(this.rootDir, 'users'),
|
|
296
|
+
node_path_1.default.join(this.rootDir, 'groups'),
|
|
297
|
+
]) {
|
|
298
|
+
const fact = await search(searchRoot);
|
|
299
|
+
if (fact) {
|
|
300
|
+
return fact;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
async getUserStats(groupId, userId) {
|
|
306
|
+
const profile = await this.readProfile(groupId, userId);
|
|
307
|
+
const dailies = await this.listRecentDailyLogs(groupId, userId, 3650);
|
|
308
|
+
const dailyFacts = dailies.reduce((sum, daily) => sum + daily.entries.length, 0);
|
|
309
|
+
const timestamps = [profile.updatedAt, ...dailies.map((daily) => daily.updatedAt)].filter((value) => value && value !== new Date(0).toISOString());
|
|
310
|
+
timestamps.sort();
|
|
311
|
+
return {
|
|
312
|
+
profileFacts: profile.facts.length,
|
|
313
|
+
dailyFacts,
|
|
314
|
+
totalFacts: profile.facts.length + dailyFacts,
|
|
315
|
+
lastUpdatedAt: timestamps.at(-1),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
toRelativePath(absolutePath) {
|
|
319
|
+
return toPosixRelative(this.rootDir, absolutePath);
|
|
320
|
+
}
|
|
321
|
+
sortFactsByUpdatedAt(facts) {
|
|
322
|
+
return [...facts].sort(compareByUpdatedAt);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
exports.TelegramFileMemoryStore = TelegramFileMemoryStore;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { StoredMemoryFact } from './file-memory';
|
|
2
|
+
export interface TelegramEmbeddingProvider {
|
|
3
|
+
readonly model: string;
|
|
4
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
5
|
+
}
|
|
6
|
+
export interface StoredLocalIndexEntry {
|
|
7
|
+
factId: string;
|
|
8
|
+
kind: 'profile' | 'daily';
|
|
9
|
+
path: string;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
content: string;
|
|
13
|
+
category: string;
|
|
14
|
+
tags: string[];
|
|
15
|
+
importance: number;
|
|
16
|
+
embedding: number[];
|
|
17
|
+
}
|
|
18
|
+
export interface StoredLocalIndex {
|
|
19
|
+
version: 1;
|
|
20
|
+
model: string;
|
|
21
|
+
updatedAt: string;
|
|
22
|
+
entries: StoredLocalIndexEntry[];
|
|
23
|
+
}
|
|
24
|
+
export declare class LocalEmbeddingProvider implements TelegramEmbeddingProvider {
|
|
25
|
+
readonly model: string;
|
|
26
|
+
private readonly dimensions;
|
|
27
|
+
constructor(config?: {
|
|
28
|
+
dimensions?: number;
|
|
29
|
+
model?: string;
|
|
30
|
+
});
|
|
31
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
32
|
+
}
|
|
33
|
+
export declare class OpenAIEmbeddingProvider implements TelegramEmbeddingProvider {
|
|
34
|
+
readonly model: string;
|
|
35
|
+
private readonly client;
|
|
36
|
+
constructor(config: {
|
|
37
|
+
apiKey: string;
|
|
38
|
+
baseURL?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
});
|
|
42
|
+
embed(texts: string[]): Promise<number[][]>;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildEmbeddingInput(fact: Pick<StoredMemoryFact, 'summary' | 'content' | 'category' | 'tags'>): string;
|
|
45
|
+
export declare function cosineSimilarity(left: number[], right: number[]): number;
|
|
46
|
+
export declare function tokenizeQuery(query: string): string[];
|
|
47
|
+
export declare function scoreIndexedEntry(params: {
|
|
48
|
+
entry: StoredLocalIndexEntry;
|
|
49
|
+
query: string;
|
|
50
|
+
queryTokens: string[];
|
|
51
|
+
queryEmbedding?: number[] | null;
|
|
52
|
+
now: Date;
|
|
53
|
+
}): number;
|
|
@@ -0,0 +1,130 @@
|
|
|
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.OpenAIEmbeddingProvider = exports.LocalEmbeddingProvider = void 0;
|
|
7
|
+
exports.buildEmbeddingInput = buildEmbeddingInput;
|
|
8
|
+
exports.cosineSimilarity = cosineSimilarity;
|
|
9
|
+
exports.tokenizeQuery = tokenizeQuery;
|
|
10
|
+
exports.scoreIndexedEntry = scoreIndexedEntry;
|
|
11
|
+
const openai_1 = __importDefault(require("openai"));
|
|
12
|
+
const DEFAULT_EMBEDDING_MODEL = 'text-embedding-3-small';
|
|
13
|
+
const DEFAULT_LOCAL_DIMENSIONS = 64;
|
|
14
|
+
function embedTextLocally(text, dimensions) {
|
|
15
|
+
const vector = new Array(dimensions).fill(0);
|
|
16
|
+
const tokens = text
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
19
|
+
.map((token) => token.trim())
|
|
20
|
+
.filter((token) => token.length > 1);
|
|
21
|
+
if (tokens.length === 0) {
|
|
22
|
+
return vector;
|
|
23
|
+
}
|
|
24
|
+
for (const token of tokens) {
|
|
25
|
+
let hash = 0;
|
|
26
|
+
for (let index = 0; index < token.length; index += 1) {
|
|
27
|
+
hash = (hash * 31 + token.charCodeAt(index)) >>> 0;
|
|
28
|
+
}
|
|
29
|
+
vector[hash % dimensions] += 1;
|
|
30
|
+
}
|
|
31
|
+
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
|
|
32
|
+
if (norm === 0) {
|
|
33
|
+
return vector;
|
|
34
|
+
}
|
|
35
|
+
return vector.map((value) => value / norm);
|
|
36
|
+
}
|
|
37
|
+
class LocalEmbeddingProvider {
|
|
38
|
+
constructor(config = {}) {
|
|
39
|
+
this.dimensions = config.dimensions ?? DEFAULT_LOCAL_DIMENSIONS;
|
|
40
|
+
this.model = config.model ?? `local-hash-${this.dimensions}`;
|
|
41
|
+
}
|
|
42
|
+
async embed(texts) {
|
|
43
|
+
return texts.map((text) => embedTextLocally(text, this.dimensions));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.LocalEmbeddingProvider = LocalEmbeddingProvider;
|
|
47
|
+
class OpenAIEmbeddingProvider {
|
|
48
|
+
constructor(config) {
|
|
49
|
+
this.client = new openai_1.default({
|
|
50
|
+
apiKey: config.apiKey,
|
|
51
|
+
baseURL: config.baseURL,
|
|
52
|
+
defaultHeaders: config.headers,
|
|
53
|
+
});
|
|
54
|
+
this.model = config.model?.trim() || DEFAULT_EMBEDDING_MODEL;
|
|
55
|
+
}
|
|
56
|
+
async embed(texts) {
|
|
57
|
+
if (texts.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
const response = await this.client.embeddings.create({
|
|
61
|
+
model: this.model,
|
|
62
|
+
input: texts,
|
|
63
|
+
});
|
|
64
|
+
return response.data
|
|
65
|
+
.slice()
|
|
66
|
+
.sort((left, right) => left.index - right.index)
|
|
67
|
+
.map((item) => item.embedding);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.OpenAIEmbeddingProvider = OpenAIEmbeddingProvider;
|
|
71
|
+
function buildEmbeddingInput(fact) {
|
|
72
|
+
return [
|
|
73
|
+
fact.summary.trim(),
|
|
74
|
+
fact.content.trim(),
|
|
75
|
+
fact.category.trim(),
|
|
76
|
+
fact.tags.join(' ').trim(),
|
|
77
|
+
]
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join('\n');
|
|
80
|
+
}
|
|
81
|
+
function cosineSimilarity(left, right) {
|
|
82
|
+
if (left.length === 0 || right.length === 0 || left.length !== right.length) {
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
let dot = 0;
|
|
86
|
+
let leftNorm = 0;
|
|
87
|
+
let rightNorm = 0;
|
|
88
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
89
|
+
dot += left[index] * right[index];
|
|
90
|
+
leftNorm += left[index] * left[index];
|
|
91
|
+
rightNorm += right[index] * right[index];
|
|
92
|
+
}
|
|
93
|
+
if (leftNorm === 0 || rightNorm === 0) {
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
return dot / (Math.sqrt(leftNorm) * Math.sqrt(rightNorm));
|
|
97
|
+
}
|
|
98
|
+
function tokenizeQuery(query) {
|
|
99
|
+
return Array.from(new Set(query
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
102
|
+
.map((token) => token.trim())
|
|
103
|
+
.filter((token) => token.length > 1)));
|
|
104
|
+
}
|
|
105
|
+
function scoreIndexedEntry(params) {
|
|
106
|
+
const haystack = [
|
|
107
|
+
params.entry.summary,
|
|
108
|
+
params.entry.content,
|
|
109
|
+
params.entry.category,
|
|
110
|
+
params.entry.tags.join(' '),
|
|
111
|
+
]
|
|
112
|
+
.join('\n')
|
|
113
|
+
.toLowerCase();
|
|
114
|
+
const lexicalMatches = params.queryTokens.filter((token) => haystack.includes(token)).length;
|
|
115
|
+
const lexicalScore = params.queryTokens.length === 0 ? 0 : lexicalMatches / params.queryTokens.length;
|
|
116
|
+
const exactPhraseBoost = haystack.includes(params.query.trim().toLowerCase()) ? 0.12 : 0;
|
|
117
|
+
const embeddingScore = params.queryEmbedding
|
|
118
|
+
? Math.max(0, cosineSimilarity(params.queryEmbedding, params.entry.embedding))
|
|
119
|
+
: 0;
|
|
120
|
+
const ageDays = Math.max(0, (params.now.getTime() - new Date(params.entry.updatedAt).getTime()) / (24 * 60 * 60 * 1000));
|
|
121
|
+
const recencyScore = params.entry.kind === 'daily' ? Math.max(0, 1 - ageDays / 14) : 0.4;
|
|
122
|
+
if (params.queryEmbedding) {
|
|
123
|
+
return (embeddingScore * 0.55 +
|
|
124
|
+
lexicalScore * 0.2 +
|
|
125
|
+
params.entry.importance * 0.15 +
|
|
126
|
+
recencyScore * 0.1 +
|
|
127
|
+
exactPhraseBoost);
|
|
128
|
+
}
|
|
129
|
+
return (lexicalScore * 0.55 + params.entry.importance * 0.25 + recencyScore * 0.2 + exactPhraseBoost);
|
|
130
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "telegram-agent-memory",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "File-first long-term memory for Telegram bots and group members",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"CHANGELOG.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"clean": "node -e \"require('fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
22
|
+
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.build.json --noEmit",
|
|
24
|
+
"format": "prettier --write .",
|
|
25
|
+
"format:check": "prettier --check .",
|
|
26
|
+
"lint": "eslint . --ext .ts",
|
|
27
|
+
"prepack": "npm run build",
|
|
28
|
+
"pack:check": "npm pack --dry-run --cache .npm-cache",
|
|
29
|
+
"release:check": "npm run verify && npm run pack:check",
|
|
30
|
+
"test": "jest --coverage --runInBand",
|
|
31
|
+
"test:unit": "jest --runInBand tests/unit/package-manifest.test.ts tests/unit/telegram-memory.test.ts tests/unit/telegram-memory-evaluation.test.ts",
|
|
32
|
+
"test:telegram-memory": "jest --runInBand tests/unit/package-manifest.test.ts tests/unit/telegram-memory.test.ts tests/unit/telegram-memory-evaluation.test.ts",
|
|
33
|
+
"test:telegram-memory:eval": "jest --runInBand tests/unit/telegram-memory-evaluation.test.ts",
|
|
34
|
+
"test:watch": "jest --watch",
|
|
35
|
+
"verify": "npm run format:check && npm run lint && npm run build && npm run typecheck && npm run test",
|
|
36
|
+
"dev": "tsc --watch"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"telegram",
|
|
40
|
+
"agent",
|
|
41
|
+
"memory",
|
|
42
|
+
"markdown",
|
|
43
|
+
"file-first",
|
|
44
|
+
"ai"
|
|
45
|
+
],
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"author": "",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"sideEffects": false,
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/jest": "^29.5.12",
|
|
54
|
+
"@types/node": "^20.12.7",
|
|
55
|
+
"@types/uuid": "^9.0.8",
|
|
56
|
+
"eslint": "^9.25.1",
|
|
57
|
+
"globals": "^16.0.0",
|
|
58
|
+
"jest": "^29.7.0",
|
|
59
|
+
"prettier": "^3.5.3",
|
|
60
|
+
"ts-jest": "^29.1.2",
|
|
61
|
+
"typescript-eslint": "^8.30.1",
|
|
62
|
+
"typescript": "^5.4.5",
|
|
63
|
+
"uuid": "^9.0.1"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"openai": "^4.28.4"
|
|
67
|
+
}
|
|
68
|
+
}
|