beast-agent 1.9.0 → 2.0.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/LICENSE +21 -21
- package/README.md +130 -130
- package/bin/beast-agent.js +137 -137
- package/package.json +1 -1
- package/scripts/fix-electron.js +138 -138
- package/scripts/release.js +137 -131
- package/scripts/swap-electron.js +40 -40
- package/src/agent/agentdefs.js +122 -122
- package/src/agent/bots.js +580 -580
- package/src/agent/bus.js +389 -389
- package/src/agent/computeruse.js +200 -200
- package/src/agent/config.js +284 -284
- package/src/agent/discord.js +332 -332
- package/src/agent/engine.js +75 -10
- package/src/agent/kb.js +123 -123
- package/src/agent/llm.js +430 -430
- package/src/agent/logger.js +90 -90
- package/src/agent/mcp.js +427 -427
- package/src/agent/mem0.js +605 -605
- package/src/agent/memory.js +427 -427
- package/src/agent/mqueue.js +124 -124
- package/src/agent/pdf.js +20 -20
- package/src/agent/research.js +133 -133
- package/src/agent/scripts/news.py +113 -113
- package/src/agent/scripts/stealthsearch.py +30 -30
- package/src/agent/scripts/websearch.py +225 -225
- package/src/agent/searxng.js +325 -325
- package/src/agent/seeds/brainstorming/SKILL.md +90 -90
- package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
- package/src/agent/seeds/executing-plans/SKILL.md +60 -60
- package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
- package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
- package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
- package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
- package/src/agent/seeds/writing-plans/SKILL.md +162 -162
- package/src/agent/seeds/writing-skills/SKILL.md +229 -229
- package/src/agent/skills.js +652 -652
- package/src/agent/store.js +378 -378
- package/src/agent/telegram.js +155 -155
- package/src/agent/tokens.js +39 -39
- package/src/agent/usage.js +125 -125
- package/src/agent/watchers.js +312 -312
- package/src/agent/watext.js +80 -80
- package/src/agent/whatsapp.js +555 -555
- package/src/cron.js +255 -255
- package/src/main.js +348 -6
- package/src/preload.js +9 -0
- package/src/renderer/browserPreload.js +73 -73
- package/src/renderer/i18n.js +4 -0
- package/src/renderer/index.html +448 -409
- package/src/renderer/renderer.js +824 -41
- package/src/renderer/style.css +264 -5
package/src/agent/mem0.js
CHANGED
|
@@ -1,605 +1,605 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* mem0-native: mem0 mimarisinin (github.com/mem0ai/mem0) Beast'e uyarlanmış
|
|
4
|
-
Node çekirdeği. Python/mem0-ts BAĞIMLILIĞI YOK:
|
|
5
|
-
- Vektör: @xenova/transformers (whisper için zaten var) → all-MiniLM-L6-v2 (384d)
|
|
6
|
-
- Store: JSON dosya per scope → bot başına AYRI dosya = tam izolasyon
|
|
7
|
-
- Konsolidasyon: mem0 klasik update prompt'u → ADD / UPDATE / DELETE / NONE
|
|
8
|
-
- Audit: JSONL geçmiş (mem0 history.db karşılığı)
|
|
9
|
-
- Hibrit arama: 0.65*semantic(cosine) + 0.35*keyword (mem0 semantic+BM25 felsefesi)
|
|
10
|
-
Her fonksiyon degradasyonlu: embedding yoksa keyword, LLM yoksa hash+cos dedup.
|
|
11
|
-
Scope: 'main' | 'bot:<botId>' — botlar arasında ASLA çapraz erişim yok. */
|
|
12
|
-
|
|
13
|
-
const fs = require('fs');
|
|
14
|
-
const path = require('path');
|
|
15
|
-
const os = require('os');
|
|
16
|
-
const crypto = require('crypto');
|
|
17
|
-
|
|
18
|
-
const CAP = 400; // scope başına maksimum kayıt (memory.js cap ile aynı)
|
|
19
|
-
const DIMS = 384; // all-MiniLM-L6-v2
|
|
20
|
-
const SEMANTIC_SKIP = 0.93; // add fast-path: bu cosine üstü aynı bilgi sayılır
|
|
21
|
-
const CAND_THRESHOLD = 0.3; // konsolidasyon adayı için min cosine
|
|
22
|
-
const CAND_K = 6; // fact başına çekilecek benzer aday
|
|
23
|
-
const SEARCH_SEM_W = 0.65; // hibrit skor: semantik ağırlık
|
|
24
|
-
const SEARCH_KW_W = 0.35; // hibrit skor: keyword ağırlık
|
|
25
|
-
const LOAD_TIMEOUT_MS = 10000; // embed modeli ilk yüklemeye bu süreyi bekler, sonra vektörsüz devam
|
|
26
|
-
|
|
27
|
-
/* ---------- yollar ---------- */
|
|
28
|
-
|
|
29
|
-
function beastRoot() {
|
|
30
|
-
if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
|
|
31
|
-
return process.env.APPDATA
|
|
32
|
-
? path.join(process.env.APPDATA, 'beast')
|
|
33
|
-
: path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function mem0Dir() {
|
|
37
|
-
const d = path.join(beastRoot(), 'memories', 'mem0');
|
|
38
|
-
try { fs.mkdirSync(d, { recursive: true }); } catch {}
|
|
39
|
-
return d;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/* scope → güvenli dosya adı ('bot:ahmet-42' → 'bot-ahmet-42') */
|
|
43
|
-
function fileSafe(scope) {
|
|
44
|
-
return String(scope || 'main').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function storePath(scope) {
|
|
48
|
-
return path.join(mem0Dir(), fileSafe(scope) + '.json');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function logPath(scope) {
|
|
52
|
-
return path.join(mem0Dir(), fileSafe(scope) + '.log');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* ---------- metin skorlama (memory.js ile aynı klasikler — öz-bağımsız) ---------- */
|
|
56
|
-
|
|
57
|
-
const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
|
|
58
|
-
|
|
59
|
-
function fold(s) {
|
|
60
|
-
return String(s || '')
|
|
61
|
-
.toLowerCase()
|
|
62
|
-
.replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function tokenize(q) {
|
|
66
|
-
return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function normText(t) {
|
|
70
|
-
return String(t || '').replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function md5(s) {
|
|
74
|
-
return crypto.createHash('md5').update(String(s || ''), 'utf8').digest('hex');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function scoreEntry(entryText, qTokens) {
|
|
78
|
-
if (!qTokens.length) return 0;
|
|
79
|
-
const hay = fold(entryText);
|
|
80
|
-
let hits = 0;
|
|
81
|
-
for (const t of new Set(qTokens)) if (hay.includes(t)) hits++;
|
|
82
|
-
return hits / Math.sqrt(qTokens.length);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/* ---------- vektör sağlayıcı ---------- */
|
|
86
|
-
|
|
87
|
-
let _embedFn = null; // test enjeksiyonu
|
|
88
|
-
let _pipe = null;
|
|
89
|
-
let _pipeLoading = null;
|
|
90
|
-
let _pipeBroken = false;
|
|
91
|
-
|
|
92
|
-
/** Test/ar-ge: harici embedder tak (async (texts) => Float32Array[] | null) */
|
|
93
|
-
function setEmbedder(fn) {
|
|
94
|
-
_embedFn = typeof fn === 'function' ? fn : null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function loadPipeline() {
|
|
98
|
-
if (_pipe) return Promise.resolve(_pipe);
|
|
99
|
-
if (_pipeLoading) return _pipeLoading;
|
|
100
|
-
if (_pipeBroken) return Promise.resolve(null);
|
|
101
|
-
_pipeLoading = (async () => {
|
|
102
|
-
try {
|
|
103
|
-
const { pipeline, env } = require('@xenova/transformers');
|
|
104
|
-
const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
|
|
105
|
-
fs.mkdirSync(modelsDir, { recursive: true });
|
|
106
|
-
env.cacheDir = modelsDir;
|
|
107
|
-
env.allowLocalModels = false;
|
|
108
|
-
const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
|
|
109
|
-
_pipe = async (texts) => {
|
|
110
|
-
const out = [];
|
|
111
|
-
for (const t of texts) {
|
|
112
|
-
const tensor = await p(t, { pooling: 'mean', normalize: true });
|
|
113
|
-
const arr = new Float32Array(DIMS);
|
|
114
|
-
const data = tensor.data;
|
|
115
|
-
for (let i = 0; i < DIMS && i < data.length; i++) arr[i] = data[i];
|
|
116
|
-
out.push(arr);
|
|
117
|
-
}
|
|
118
|
-
return out;
|
|
119
|
-
};
|
|
120
|
-
return _pipe;
|
|
121
|
-
} catch {
|
|
122
|
-
_pipeBroken = true; // bir daha deneme — keyword-only moda düş
|
|
123
|
-
return null;
|
|
124
|
-
} finally {
|
|
125
|
-
_pipeLoading = null;
|
|
126
|
-
}
|
|
127
|
-
})();
|
|
128
|
-
return _pipeLoading;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/* embedder hazırsa döner; ilk yükleme LOAD_TIMEOUT_MS'ı geçerse null (vektörsüz devam) */
|
|
132
|
-
async function getEmbedder() {
|
|
133
|
-
if (_embedFn) return _embedFn;
|
|
134
|
-
const p = loadPipeline();
|
|
135
|
-
let timer;
|
|
136
|
-
const timeout = new Promise((res) => { timer = setTimeout(() => res(null), LOAD_TIMEOUT_MS); });
|
|
137
|
-
const loaded = await Promise.race([p, timeout]).catch(() => null);
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
return loaded || null;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function embed(texts) {
|
|
143
|
-
const fn = await getEmbedder();
|
|
144
|
-
if (!fn) return texts.map(() => null);
|
|
145
|
-
try {
|
|
146
|
-
const vecs = await fn(texts);
|
|
147
|
-
if (!Array.isArray(vecs) || vecs.length !== texts.length) return texts.map(() => null);
|
|
148
|
-
return vecs.map((v) => (v instanceof Float32Array && v.length === DIMS ? v : null));
|
|
149
|
-
} catch {
|
|
150
|
-
return texts.map(() => null);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function cos(a, b) {
|
|
155
|
-
if (!a || !b) return 0;
|
|
156
|
-
let dot = 0;
|
|
157
|
-
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
158
|
-
return dot; // normalize vektörler → dot = cosine
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function vecToB64(v) {
|
|
162
|
-
if (!v) return null;
|
|
163
|
-
return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('base64');
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function b64ToVec(s) {
|
|
167
|
-
if (!s) return null;
|
|
168
|
-
try {
|
|
169
|
-
const buf = Buffer.from(String(s), 'base64');
|
|
170
|
-
if (buf.length !== DIMS * 4) return null;
|
|
171
|
-
return new Float32Array(buf.buffer, buf.byteOffset, DIMS);
|
|
172
|
-
} catch {
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/* ---------- store ---------- */
|
|
178
|
-
|
|
179
|
-
/* mevcut ayna dosyasından satırlar (MEMORY.md) — boot import + admin düzenleme sonrası */
|
|
180
|
-
function mirrorLines(scope) {
|
|
181
|
-
try {
|
|
182
|
-
let p = null;
|
|
183
|
-
if (scope === 'main') p = path.join(beastRoot(), 'memories', 'MEMORY.md');
|
|
184
|
-
else if (scope.startsWith('bot:')) {
|
|
185
|
-
const bots = require('./bots');
|
|
186
|
-
p = bots.memPath(scope.slice(4));
|
|
187
|
-
}
|
|
188
|
-
if (!p) return [];
|
|
189
|
-
return fs
|
|
190
|
-
.readFileSync(p, 'utf8')
|
|
191
|
-
.split('\n')
|
|
192
|
-
.map((l) => l.replace(/^[-*]\s*/, '').trim())
|
|
193
|
-
.filter(Boolean);
|
|
194
|
-
} catch {
|
|
195
|
-
return [];
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function loadStore(scope) {
|
|
200
|
-
try {
|
|
201
|
-
const raw = fs.readFileSync(storePath(scope), 'utf8');
|
|
202
|
-
const data = JSON.parse(raw);
|
|
203
|
-
if (data && Array.isArray(data.items)) return data;
|
|
204
|
-
} catch {}
|
|
205
|
-
/* store dosyası yok → mevcut MEMORY.md satırlarından import (vektörsüz, backfill sonra) */
|
|
206
|
-
const items = mirrorLines(scope).map((text) => ({
|
|
207
|
-
id: crypto.randomUUID(),
|
|
208
|
-
text: normText(text),
|
|
209
|
-
hash: md5(normText(text)),
|
|
210
|
-
vec: null,
|
|
211
|
-
created_at: new Date().toISOString(),
|
|
212
|
-
updated_at: new Date().toISOString(),
|
|
213
|
-
}));
|
|
214
|
-
return { version: 1, items };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function saveStore(scope, store) {
|
|
218
|
-
try {
|
|
219
|
-
fs.writeFileSync(storePath(scope), JSON.stringify(store), 'utf8');
|
|
220
|
-
return true;
|
|
221
|
-
} catch {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/* audit günlüğü (mem0 history.db karşılığı — JSONL) */
|
|
227
|
-
function audit(scope, entry) {
|
|
228
|
-
try {
|
|
229
|
-
fs.appendFileSync(logPath(scope), JSON.stringify({ at: new Date().toISOString(), ...entry }) + '\n');
|
|
230
|
-
} catch {}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function history(scope, limit = 50) {
|
|
234
|
-
try {
|
|
235
|
-
const rows = fs
|
|
236
|
-
.readFileSync(logPath(scope), 'utf8')
|
|
237
|
-
.split('\n')
|
|
238
|
-
.filter(Boolean)
|
|
239
|
-
.map((l) => {
|
|
240
|
-
try { return JSON.parse(l); } catch { return null; }
|
|
241
|
-
})
|
|
242
|
-
.filter(Boolean);
|
|
243
|
-
return rows.slice(-limit).reverse();
|
|
244
|
-
} catch {
|
|
245
|
-
return [];
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/* MEMORY.md aynasını store'dan yeniden yaz (store = doğruluk kaynağı) */
|
|
250
|
-
function syncMirror(scope) {
|
|
251
|
-
try {
|
|
252
|
-
const store = loadStore(scope);
|
|
253
|
-
const body = store.items.map((it) => '- ' + it.text).join('\n') + (store.items.length ? '\n' : '');
|
|
254
|
-
let p = null;
|
|
255
|
-
if (scope === 'main') {
|
|
256
|
-
const memory = require('./memory');
|
|
257
|
-
p = path.join(memory.memDir(), 'MEMORY.md');
|
|
258
|
-
} else if (scope.startsWith('bot:')) {
|
|
259
|
-
const bots = require('./bots');
|
|
260
|
-
p = bots.memPath(scope.slice(4));
|
|
261
|
-
}
|
|
262
|
-
if (!p) return false;
|
|
263
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
264
|
-
fs.writeFileSync(p, body, 'utf8');
|
|
265
|
-
return true;
|
|
266
|
-
} catch {
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/* admin/UI MEMORY.md'yi elle değiştirdiyse store'u satırlardan yeniden kur */
|
|
272
|
-
function reindexFromLines(scope, lines) {
|
|
273
|
-
const items = (Array.isArray(lines) ? lines : [])
|
|
274
|
-
.map((t) => normText(t))
|
|
275
|
-
.filter(Boolean)
|
|
276
|
-
.map((text) => ({
|
|
277
|
-
id: crypto.randomUUID(),
|
|
278
|
-
text,
|
|
279
|
-
hash: md5(text),
|
|
280
|
-
vec: null,
|
|
281
|
-
created_at: new Date().toISOString(),
|
|
282
|
-
updated_at: new Date().toISOString(),
|
|
283
|
-
}));
|
|
284
|
-
const store = { version: 1, items };
|
|
285
|
-
saveStore(scope, store);
|
|
286
|
-
audit(scope, { event: 'IMPORT', count: items.length });
|
|
287
|
-
scheduleBackfill(scope);
|
|
288
|
-
return { ok: true, count: items.length };
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/* ---------- arka plan vektör doldurma ---------- */
|
|
292
|
-
|
|
293
|
-
const _backfillRunning = new Set();
|
|
294
|
-
|
|
295
|
-
function scheduleBackfill(scope) {
|
|
296
|
-
if (_backfillRunning.has(scope)) return;
|
|
297
|
-
_backfillRunning.add(scope);
|
|
298
|
-
setTimeout(() => {
|
|
299
|
-
_backfill(scope)
|
|
300
|
-
.catch(() => {})
|
|
301
|
-
.finally(() => _backfillRunning.delete(scope));
|
|
302
|
-
}, 1200).unref();
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
async function _backfill(scope) {
|
|
306
|
-
const fn = await getEmbedder();
|
|
307
|
-
if (!fn) return;
|
|
308
|
-
const store = loadStore(scope);
|
|
309
|
-
const todo = store.items.filter((it) => !it.vec).slice(0, 64);
|
|
310
|
-
if (!todo.length) return;
|
|
311
|
-
const texts = todo.map((it) => it.text);
|
|
312
|
-
const vecs = await embed(texts);
|
|
313
|
-
todo.forEach((it, i) => {
|
|
314
|
-
if (vecs[i]) it.vec = vecToB64(vecs[i]);
|
|
315
|
-
});
|
|
316
|
-
saveStore(scope, store);
|
|
317
|
-
if (store.items.some((it) => !it.vec)) scheduleBackfill(scope);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/* ---------- mem0 klasik konsolidasyon ---------- */
|
|
321
|
-
|
|
322
|
-
/* mem0 update prompt'u: yeni fact + benzer mevcut kayıtlar → tek LLM karar çağrısı.
|
|
323
|
-
ID'ler indeks ("0","1",...) — LLM'in UUID halüsinasyonuna karşı (mem0 anti-hallucination). */
|
|
324
|
-
function buildUpdatePrompt(facts, candidates) {
|
|
325
|
-
const sys =
|
|
326
|
-
'Sen bir uzun vadeli hafıza yöneticisisin. Yeni bilgi (fact) ile mevcut kayıtları karşılaştırıp ' +
|
|
327
|
-
'her fact için TEK karar ver. KURALLAR:\n' +
|
|
328
|
-
'1. ADD: hafızada olmayan yeni bilgi → {"event":"ADD","text":"<fact>"}\n' +
|
|
329
|
-
'2. UPDATE: mevcut kayıtla AYNI konu ama bilgi değişmiş/güncellenmiş → ' +
|
|
330
|
-
'{"event":"UPDATE","id":"<kayıt-no>","text":"<en kapsamlı güncel hali>"}\n' +
|
|
331
|
-
'3. DELETE: fact mevcut kayıtla ÇELİŞİYOR ve kayıt artık yanlışsa → {"event":"DELETE","id":"<kayıt-no>"}\n' +
|
|
332
|
-
'4. NONE: fact zaten mevcut kayıtta var (anlamca aynı) → {"event":"NONE"}\n' +
|
|
333
|
-
'Sadece kalıcı değeri olan bilgiler; geçici detay için NONE yerine ADD yapma — ' +
|
|
334
|
-
'gereksizse {"event":"DROP"} dön. Çıktı: SADECE JSON {"events":[...]} — fact sırasıyla.';
|
|
335
|
-
const candList = candidates
|
|
336
|
-
.map((c, i) => `{"id":"${i}","text":${JSON.stringify(c.item.text)}}`)
|
|
337
|
-
.join('\n');
|
|
338
|
-
const factList = facts.map((f, i) => `${i + 1}. ${f}`).join('\n');
|
|
339
|
-
const user =
|
|
340
|
-
'MEVCUT KAYITLAR:\n' + (candList || '(boş)') + '\n\n' +
|
|
341
|
-
'YENİ BİLGİLER:\n' + factList + '\n\n' +
|
|
342
|
-
'Her yeni bilgi için sırayla karar ver. SADECE JSON dön: {"events":[{"event":"ADD","text":"..."},{"event":"UPDATE","id":"3","text":"..."},{"event":"DELETE","id":"2"},{"event":"NONE"},{"event":"DROP"}]}';
|
|
343
|
-
return { sys, user };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function parseEvents(raw, candidateCount, factCount) {
|
|
347
|
-
try {
|
|
348
|
-
const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
|
|
349
|
-
const events = Array.isArray(parsed.events) ? parsed.events : [];
|
|
350
|
-
/* fact başına 1 karar kuralı; ama DELETE+ADD "değiştirme" ikilisi meşru — pay bırak */
|
|
351
|
-
return events.slice(0, factCount + 2).filter((e) => e && typeof e === 'object');
|
|
352
|
-
} catch {
|
|
353
|
-
return null; // parse edilemedi → fast-path fallback
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/* idx ("3") → gerçek item id (anti-hallucination: harici eşleme) */
|
|
358
|
-
function resolveCandidate(candidates, idStr) {
|
|
359
|
-
const n = Number(String(idStr).replace(/[^0-9]/g, ''));
|
|
360
|
-
if (!Number.isInteger(n) || n < 0 || n >= candidates.length) return null;
|
|
361
|
-
return candidates[n];
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/* ---------- public API ---------- */
|
|
365
|
-
|
|
366
|
-
/** Kayıt ekleme (mem0 add). llm verilirse klasik konsolidasyon, yoksa hash+cos dedup. */
|
|
367
|
-
async function add(scope, texts, { llm } = {}) {
|
|
368
|
-
const list = (Array.isArray(texts) ? texts : [texts])
|
|
369
|
-
.map((t) => normText(t))
|
|
370
|
-
.filter(Boolean)
|
|
371
|
-
.slice(0, 8);
|
|
372
|
-
if (!list.length) return { ok: false, error: 'empty' };
|
|
373
|
-
const store = loadStore(scope);
|
|
374
|
-
const newVecs = await embed(list);
|
|
375
|
-
|
|
376
|
-
const findCandidates = () => {
|
|
377
|
-
/* tüm yeni fact'lerin adaylarının birleşimi (mem0: per-fact retrieval → tek karar çağrısı) */
|
|
378
|
-
const best = new Map(); // item.id → { item, score }
|
|
379
|
-
for (const t of list) {
|
|
380
|
-
const tokens = tokenize(t);
|
|
381
|
-
for (let i = 0; i < list.length; i++) {
|
|
382
|
-
const qvec = newVecs[i];
|
|
383
|
-
for (const item of store.items) {
|
|
384
|
-
const ivec = b64ToVec(item.vec);
|
|
385
|
-
const sem = qvec && ivec ? cos(qvec, ivec) : 0;
|
|
386
|
-
const kw = scoreEntry(item.text, tokens);
|
|
387
|
-
const score = Math.max(sem, kw);
|
|
388
|
-
if (score < CAND_THRESHOLD) continue;
|
|
389
|
-
const prev = best.get(item.id);
|
|
390
|
-
if (!prev || prev.score < score) best.set(item.id, { item, score });
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
return [...best.values()].sort((a, b) => b.score - a.score).slice(0, CAND_K * list.length);
|
|
395
|
-
};
|
|
396
|
-
|
|
397
|
-
const applyAdd = (text, vec) => {
|
|
398
|
-
const now = new Date().toISOString();
|
|
399
|
-
store.items.push({
|
|
400
|
-
id: crypto.randomUUID(),
|
|
401
|
-
text,
|
|
402
|
-
hash: md5(text),
|
|
403
|
-
vec: vec ? vecToB64(vec) : null,
|
|
404
|
-
created_at: now,
|
|
405
|
-
updated_at: now,
|
|
406
|
-
});
|
|
407
|
-
audit(scope, { event: 'ADD', text });
|
|
408
|
-
return { event: 'ADD', text };
|
|
409
|
-
};
|
|
410
|
-
const applyUpdate = (item, text, vec) => {
|
|
411
|
-
const old = item.text;
|
|
412
|
-
item.text = text;
|
|
413
|
-
item.hash = md5(text);
|
|
414
|
-
if (vec) item.vec = vecToB64(vec);
|
|
415
|
-
else item.vec = null;
|
|
416
|
-
item.updated_at = new Date().toISOString();
|
|
417
|
-
audit(scope, { event: 'UPDATE', id: item.id, old_text: old, text });
|
|
418
|
-
return { event: 'UPDATE', id: item.id, text };
|
|
419
|
-
};
|
|
420
|
-
const applyDelete = (item) => {
|
|
421
|
-
store.items = store.items.filter((it) => it.id !== item.id);
|
|
422
|
-
audit(scope, { event: 'DELETE', id: item.id, old_text: item.text });
|
|
423
|
-
return { event: 'DELETE', id: item.id };
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
const applied = [];
|
|
427
|
-
let usedLlm = false;
|
|
428
|
-
|
|
429
|
-
if (typeof llm === 'function') {
|
|
430
|
-
try {
|
|
431
|
-
const candidates = findCandidates();
|
|
432
|
-
const { sys, user } = buildUpdatePrompt(list, candidates);
|
|
433
|
-
const raw = await llm(sys, user);
|
|
434
|
-
const events = parseEvents(raw, candidates.length, list.length);
|
|
435
|
-
if (events) {
|
|
436
|
-
usedLlm = true;
|
|
437
|
-
for (const ev of events) {
|
|
438
|
-
const evName = String(ev.event || '').toUpperCase();
|
|
439
|
-
const cand = ev.id != null ? resolveCandidate(candidates, ev.id) : null;
|
|
440
|
-
if (evName === 'ADD' && ev.text) {
|
|
441
|
-
const t = normText(ev.text);
|
|
442
|
-
if (store.items.some((it) => it.hash === md5(t))) continue;
|
|
443
|
-
applied.push(applyAdd(t, null)); /* vektör backfill'de */
|
|
444
|
-
} else if (evName === 'UPDATE' && cand && ev.text) {
|
|
445
|
-
applied.push(applyUpdate(cand.item, normText(ev.text), null));
|
|
446
|
-
} else if (evName === 'DELETE' && cand) {
|
|
447
|
-
applied.push(applyDelete(cand.item));
|
|
448
|
-
} else if (evName === 'NONE' || evName === 'DROP') {
|
|
449
|
-
applied.push({ event: evName === 'DROP' ? 'DROP' : 'NONE' });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
} catch {}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
if (!usedLlm) {
|
|
457
|
-
/* fast-path: hash dup → skip; SEMANTIC_SKIP üstü cosine → skip; yoksa ADD */
|
|
458
|
-
for (let i = 0; i < list.length; i++) {
|
|
459
|
-
const t = list[i];
|
|
460
|
-
const vec = newVecs[i];
|
|
461
|
-
if (store.items.some((it) => it.hash === md5(t))) {
|
|
462
|
-
applied.push({ event: 'NONE', text: t });
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
let twin = false;
|
|
466
|
-
if (vec) {
|
|
467
|
-
for (const it of store.items) {
|
|
468
|
-
const ivec = b64ToVec(it.vec);
|
|
469
|
-
if (ivec && cos(vec, ivec) >= SEMANTIC_SKIP) { twin = true; break; }
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
if (twin) {
|
|
473
|
-
applied.push({ event: 'NONE', text: t });
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
applied.push(applyAdd(t, vec));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/* cap aşımı: en eskilerden düş */
|
|
481
|
-
if (store.items.length > CAP) {
|
|
482
|
-
const overflow = store.items.length - CAP;
|
|
483
|
-
const dropped = store.items.splice(0, overflow);
|
|
484
|
-
for (const d of dropped) audit(scope, { event: 'CAP_DROP', id: d.id, old_text: d.text });
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
saveStore(scope, store);
|
|
488
|
-
scheduleBackfill(scope);
|
|
489
|
-
return {
|
|
490
|
-
ok: true,
|
|
491
|
-
llm: usedLlm,
|
|
492
|
-
added: applied.filter((e) => e.event === 'ADD').length,
|
|
493
|
-
updated: applied.filter((e) => e.event === 'UPDATE').length,
|
|
494
|
-
deleted: applied.filter((e) => e.event === 'DELETE').length,
|
|
495
|
-
skipped: applied.filter((e) => e.event === 'NONE' || e.event === 'DROP').length,
|
|
496
|
-
events: applied,
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
/** Hibrit arama: 0.65*cosine + 0.35*keyword; vektör yoksa pure keyword (degradasyon). */
|
|
501
|
-
async function search(scope, query, { limit = 8, threshold = 0.05 } = {}) {
|
|
502
|
-
const q = String(query || '').trim();
|
|
503
|
-
const store = loadStore(scope);
|
|
504
|
-
if (!store.items.length || !q) return [];
|
|
505
|
-
const qTokens = tokenize(q);
|
|
506
|
-
const qvec = (await embed([q]))[0];
|
|
507
|
-
const rows = store.items.map((item) => {
|
|
508
|
-
const ivec = b64ToVec(item.vec);
|
|
509
|
-
const sem = qvec && ivec ? Math.max(0, cos(qvec, ivec)) : 0;
|
|
510
|
-
const kw = scoreEntry(item.text, qTokens);
|
|
511
|
-
const score = qvec && ivec
|
|
512
|
-
? SEARCH_SEM_W * sem + SEARCH_KW_W * kw
|
|
513
|
-
: kw;
|
|
514
|
-
return { id: item.id, text: item.text, score: Number(score.toFixed(3)), updated_at: item.updated_at };
|
|
515
|
-
});
|
|
516
|
-
return rows
|
|
517
|
-
.filter((r) => r.score > threshold)
|
|
518
|
-
.sort((a, b) => b.score - a.score)
|
|
519
|
-
.slice(0, limit);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/** Prompt için ilgili kayıtlar ('- ' satırları; memory.relevantFor uyumlu çıktı). */
|
|
523
|
-
async function relevant(scope, query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
|
|
524
|
-
const store = loadStore(scope);
|
|
525
|
-
if (!store.items.length) return '';
|
|
526
|
-
const picked = new Set();
|
|
527
|
-
try {
|
|
528
|
-
const hits = await search(scope, query, { limit: maxRelevant, threshold: 0.05 });
|
|
529
|
-
for (const h of hits) picked.add(h.id);
|
|
530
|
-
} catch {}
|
|
531
|
-
/* en yeniler de daima girer (memory.relevantFor davranışı) */
|
|
532
|
-
for (let i = store.items.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
|
|
533
|
-
picked.add(store.items[i].id);
|
|
534
|
-
}
|
|
535
|
-
/* çıktı: insertion sırası korunur (ayna dosyasıyla tutarlı) */
|
|
536
|
-
let out = '';
|
|
537
|
-
for (const it of store.items) {
|
|
538
|
-
if (!picked.has(it.id)) continue;
|
|
539
|
-
const line = '- ' + it.text;
|
|
540
|
-
if (out.length + line.length > charCap) break;
|
|
541
|
-
out += (out ? '\n' : '') + line;
|
|
542
|
-
}
|
|
543
|
-
return out;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function recentTexts(scope, n = 30) {
|
|
547
|
-
try {
|
|
548
|
-
return loadStore(scope).items.slice(-n).map((it) => it.text);
|
|
549
|
-
} catch {
|
|
550
|
-
return [];
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function stats(scope) {
|
|
555
|
-
const store = loadStore(scope);
|
|
556
|
-
return {
|
|
557
|
-
scope: String(scope),
|
|
558
|
-
items: store.items.length,
|
|
559
|
-
withVec: store.items.filter((it) => it.vec).length,
|
|
560
|
-
cap: CAP,
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
/** mem0 FACT_RETRIEVAL (TR uyarlaması): sohbetten kalıcı fact'ler çıkarır. */
|
|
565
|
-
async function extractFacts(llm, transcript, existing, { max = 3 } = {}) {
|
|
566
|
-
if (typeof llm !== 'function') return [];
|
|
567
|
-
const sys =
|
|
568
|
-
'Aşağıdaki konuşma parçasından UZUN VADELİ hatırlanmaya değer bilgileri çıkar ' +
|
|
569
|
-
'(kullanıcının kalıcı tercihleri, projeleri/işleri, isimler, rutinler, düzeltmeler).\n' +
|
|
570
|
-
'KURALLAR:\n' +
|
|
571
|
-
`- Sadece haftalar sonra bile işe yarayacak şeyler; geçici detay asla\n` +
|
|
572
|
-
`- En fazla ${max} madde, her biri max 120 karakter, "kullanıcı ..." diye başla\n` +
|
|
573
|
-
'- ZATEN KAYITLI olanlarla anlamca aynıysa tekrar ekleme\n' +
|
|
574
|
-
'- Değerli bilgi yoksa boş dizi\n' +
|
|
575
|
-
'SADECE JSON dön: {"memories": ["...", "..."]} veya {"memories": []}';
|
|
576
|
-
const user =
|
|
577
|
-
'ZATEN KAYITLI olanlar (tekrar ekleme):\n' + (existing || '(boş)') + '\n\n' +
|
|
578
|
-
'# SOHBET\n' + transcript;
|
|
579
|
-
try {
|
|
580
|
-
const raw = await llm(sys, user);
|
|
581
|
-
const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
|
|
582
|
-
const arr = parsed && Array.isArray(parsed.memories) ? parsed.memories : [];
|
|
583
|
-
return arr.map((m) => normText(m)).filter(Boolean).slice(0, max);
|
|
584
|
-
} catch {
|
|
585
|
-
return [];
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
module.exports = {
|
|
590
|
-
add,
|
|
591
|
-
search,
|
|
592
|
-
relevant,
|
|
593
|
-
recentTexts,
|
|
594
|
-
extractFacts,
|
|
595
|
-
syncMirror,
|
|
596
|
-
reindexFromLines,
|
|
597
|
-
history,
|
|
598
|
-
stats,
|
|
599
|
-
setEmbedder,
|
|
600
|
-
mirrorLines,
|
|
601
|
-
CAP,
|
|
602
|
-
DIMS,
|
|
603
|
-
SEMANTIC_SKIP,
|
|
604
|
-
CAND_THRESHOLD,
|
|
605
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* mem0-native: mem0 mimarisinin (github.com/mem0ai/mem0) Beast'e uyarlanmış
|
|
4
|
+
Node çekirdeği. Python/mem0-ts BAĞIMLILIĞI YOK:
|
|
5
|
+
- Vektör: @xenova/transformers (whisper için zaten var) → all-MiniLM-L6-v2 (384d)
|
|
6
|
+
- Store: JSON dosya per scope → bot başına AYRI dosya = tam izolasyon
|
|
7
|
+
- Konsolidasyon: mem0 klasik update prompt'u → ADD / UPDATE / DELETE / NONE
|
|
8
|
+
- Audit: JSONL geçmiş (mem0 history.db karşılığı)
|
|
9
|
+
- Hibrit arama: 0.65*semantic(cosine) + 0.35*keyword (mem0 semantic+BM25 felsefesi)
|
|
10
|
+
Her fonksiyon degradasyonlu: embedding yoksa keyword, LLM yoksa hash+cos dedup.
|
|
11
|
+
Scope: 'main' | 'bot:<botId>' — botlar arasında ASLA çapraz erişim yok. */
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
|
|
18
|
+
const CAP = 400; // scope başına maksimum kayıt (memory.js cap ile aynı)
|
|
19
|
+
const DIMS = 384; // all-MiniLM-L6-v2
|
|
20
|
+
const SEMANTIC_SKIP = 0.93; // add fast-path: bu cosine üstü aynı bilgi sayılır
|
|
21
|
+
const CAND_THRESHOLD = 0.3; // konsolidasyon adayı için min cosine
|
|
22
|
+
const CAND_K = 6; // fact başına çekilecek benzer aday
|
|
23
|
+
const SEARCH_SEM_W = 0.65; // hibrit skor: semantik ağırlık
|
|
24
|
+
const SEARCH_KW_W = 0.35; // hibrit skor: keyword ağırlık
|
|
25
|
+
const LOAD_TIMEOUT_MS = 10000; // embed modeli ilk yüklemeye bu süreyi bekler, sonra vektörsüz devam
|
|
26
|
+
|
|
27
|
+
/* ---------- yollar ---------- */
|
|
28
|
+
|
|
29
|
+
function beastRoot() {
|
|
30
|
+
if (process.env.BEAST_DATA) return process.env.BEAST_DATA;
|
|
31
|
+
return process.env.APPDATA
|
|
32
|
+
? path.join(process.env.APPDATA, 'beast')
|
|
33
|
+
: path.join(os.homedir(), 'AppData', 'Roaming', 'beast');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mem0Dir() {
|
|
37
|
+
const d = path.join(beastRoot(), 'memories', 'mem0');
|
|
38
|
+
try { fs.mkdirSync(d, { recursive: true }); } catch {}
|
|
39
|
+
return d;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* scope → güvenli dosya adı ('bot:ahmet-42' → 'bot-ahmet-42') */
|
|
43
|
+
function fileSafe(scope) {
|
|
44
|
+
return String(scope || 'main').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function storePath(scope) {
|
|
48
|
+
return path.join(mem0Dir(), fileSafe(scope) + '.json');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function logPath(scope) {
|
|
52
|
+
return path.join(mem0Dir(), fileSafe(scope) + '.log');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ---------- metin skorlama (memory.js ile aynı klasikler — öz-bağımsız) ---------- */
|
|
56
|
+
|
|
57
|
+
const TR_FOLD = { ç: 'c', ğ: 'g', ı: 'i', ö: 'o', ş: 's', ü: 'u', â: 'a', î: 'i', û: 'u' };
|
|
58
|
+
|
|
59
|
+
function fold(s) {
|
|
60
|
+
return String(s || '')
|
|
61
|
+
.toLowerCase()
|
|
62
|
+
.replace(/[çğıöşüâîû]/g, (ch) => TR_FOLD[ch] || ch);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function tokenize(q) {
|
|
66
|
+
return (fold(q).match(/[a-z0-9_+#.]+/g) || []).filter((w) => w.length >= 3);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normText(t) {
|
|
70
|
+
return String(t || '').replace(/\s+/g, ' ').trim().slice(0, 500);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function md5(s) {
|
|
74
|
+
return crypto.createHash('md5').update(String(s || ''), 'utf8').digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function scoreEntry(entryText, qTokens) {
|
|
78
|
+
if (!qTokens.length) return 0;
|
|
79
|
+
const hay = fold(entryText);
|
|
80
|
+
let hits = 0;
|
|
81
|
+
for (const t of new Set(qTokens)) if (hay.includes(t)) hits++;
|
|
82
|
+
return hits / Math.sqrt(qTokens.length);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ---------- vektör sağlayıcı ---------- */
|
|
86
|
+
|
|
87
|
+
let _embedFn = null; // test enjeksiyonu
|
|
88
|
+
let _pipe = null;
|
|
89
|
+
let _pipeLoading = null;
|
|
90
|
+
let _pipeBroken = false;
|
|
91
|
+
|
|
92
|
+
/** Test/ar-ge: harici embedder tak (async (texts) => Float32Array[] | null) */
|
|
93
|
+
function setEmbedder(fn) {
|
|
94
|
+
_embedFn = typeof fn === 'function' ? fn : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadPipeline() {
|
|
98
|
+
if (_pipe) return Promise.resolve(_pipe);
|
|
99
|
+
if (_pipeLoading) return _pipeLoading;
|
|
100
|
+
if (_pipeBroken) return Promise.resolve(null);
|
|
101
|
+
_pipeLoading = (async () => {
|
|
102
|
+
try {
|
|
103
|
+
const { pipeline, env } = require('@xenova/transformers');
|
|
104
|
+
const modelsDir = process.env.BEAST_MODELS_DIR || path.join(beastRoot(), 'models');
|
|
105
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
106
|
+
env.cacheDir = modelsDir;
|
|
107
|
+
env.allowLocalModels = false;
|
|
108
|
+
const p = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
|
|
109
|
+
_pipe = async (texts) => {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const t of texts) {
|
|
112
|
+
const tensor = await p(t, { pooling: 'mean', normalize: true });
|
|
113
|
+
const arr = new Float32Array(DIMS);
|
|
114
|
+
const data = tensor.data;
|
|
115
|
+
for (let i = 0; i < DIMS && i < data.length; i++) arr[i] = data[i];
|
|
116
|
+
out.push(arr);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
return _pipe;
|
|
121
|
+
} catch {
|
|
122
|
+
_pipeBroken = true; // bir daha deneme — keyword-only moda düş
|
|
123
|
+
return null;
|
|
124
|
+
} finally {
|
|
125
|
+
_pipeLoading = null;
|
|
126
|
+
}
|
|
127
|
+
})();
|
|
128
|
+
return _pipeLoading;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* embedder hazırsa döner; ilk yükleme LOAD_TIMEOUT_MS'ı geçerse null (vektörsüz devam) */
|
|
132
|
+
async function getEmbedder() {
|
|
133
|
+
if (_embedFn) return _embedFn;
|
|
134
|
+
const p = loadPipeline();
|
|
135
|
+
let timer;
|
|
136
|
+
const timeout = new Promise((res) => { timer = setTimeout(() => res(null), LOAD_TIMEOUT_MS); });
|
|
137
|
+
const loaded = await Promise.race([p, timeout]).catch(() => null);
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
return loaded || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function embed(texts) {
|
|
143
|
+
const fn = await getEmbedder();
|
|
144
|
+
if (!fn) return texts.map(() => null);
|
|
145
|
+
try {
|
|
146
|
+
const vecs = await fn(texts);
|
|
147
|
+
if (!Array.isArray(vecs) || vecs.length !== texts.length) return texts.map(() => null);
|
|
148
|
+
return vecs.map((v) => (v instanceof Float32Array && v.length === DIMS ? v : null));
|
|
149
|
+
} catch {
|
|
150
|
+
return texts.map(() => null);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function cos(a, b) {
|
|
155
|
+
if (!a || !b) return 0;
|
|
156
|
+
let dot = 0;
|
|
157
|
+
for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
|
|
158
|
+
return dot; // normalize vektörler → dot = cosine
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function vecToB64(v) {
|
|
162
|
+
if (!v) return null;
|
|
163
|
+
return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('base64');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function b64ToVec(s) {
|
|
167
|
+
if (!s) return null;
|
|
168
|
+
try {
|
|
169
|
+
const buf = Buffer.from(String(s), 'base64');
|
|
170
|
+
if (buf.length !== DIMS * 4) return null;
|
|
171
|
+
return new Float32Array(buf.buffer, buf.byteOffset, DIMS);
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ---------- store ---------- */
|
|
178
|
+
|
|
179
|
+
/* mevcut ayna dosyasından satırlar (MEMORY.md) — boot import + admin düzenleme sonrası */
|
|
180
|
+
function mirrorLines(scope) {
|
|
181
|
+
try {
|
|
182
|
+
let p = null;
|
|
183
|
+
if (scope === 'main') p = path.join(beastRoot(), 'memories', 'MEMORY.md');
|
|
184
|
+
else if (scope.startsWith('bot:')) {
|
|
185
|
+
const bots = require('./bots');
|
|
186
|
+
p = bots.memPath(scope.slice(4));
|
|
187
|
+
}
|
|
188
|
+
if (!p) return [];
|
|
189
|
+
return fs
|
|
190
|
+
.readFileSync(p, 'utf8')
|
|
191
|
+
.split('\n')
|
|
192
|
+
.map((l) => l.replace(/^[-*]\s*/, '').trim())
|
|
193
|
+
.filter(Boolean);
|
|
194
|
+
} catch {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function loadStore(scope) {
|
|
200
|
+
try {
|
|
201
|
+
const raw = fs.readFileSync(storePath(scope), 'utf8');
|
|
202
|
+
const data = JSON.parse(raw);
|
|
203
|
+
if (data && Array.isArray(data.items)) return data;
|
|
204
|
+
} catch {}
|
|
205
|
+
/* store dosyası yok → mevcut MEMORY.md satırlarından import (vektörsüz, backfill sonra) */
|
|
206
|
+
const items = mirrorLines(scope).map((text) => ({
|
|
207
|
+
id: crypto.randomUUID(),
|
|
208
|
+
text: normText(text),
|
|
209
|
+
hash: md5(normText(text)),
|
|
210
|
+
vec: null,
|
|
211
|
+
created_at: new Date().toISOString(),
|
|
212
|
+
updated_at: new Date().toISOString(),
|
|
213
|
+
}));
|
|
214
|
+
return { version: 1, items };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function saveStore(scope, store) {
|
|
218
|
+
try {
|
|
219
|
+
fs.writeFileSync(storePath(scope), JSON.stringify(store), 'utf8');
|
|
220
|
+
return true;
|
|
221
|
+
} catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* audit günlüğü (mem0 history.db karşılığı — JSONL) */
|
|
227
|
+
function audit(scope, entry) {
|
|
228
|
+
try {
|
|
229
|
+
fs.appendFileSync(logPath(scope), JSON.stringify({ at: new Date().toISOString(), ...entry }) + '\n');
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function history(scope, limit = 50) {
|
|
234
|
+
try {
|
|
235
|
+
const rows = fs
|
|
236
|
+
.readFileSync(logPath(scope), 'utf8')
|
|
237
|
+
.split('\n')
|
|
238
|
+
.filter(Boolean)
|
|
239
|
+
.map((l) => {
|
|
240
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
241
|
+
})
|
|
242
|
+
.filter(Boolean);
|
|
243
|
+
return rows.slice(-limit).reverse();
|
|
244
|
+
} catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* MEMORY.md aynasını store'dan yeniden yaz (store = doğruluk kaynağı) */
|
|
250
|
+
function syncMirror(scope) {
|
|
251
|
+
try {
|
|
252
|
+
const store = loadStore(scope);
|
|
253
|
+
const body = store.items.map((it) => '- ' + it.text).join('\n') + (store.items.length ? '\n' : '');
|
|
254
|
+
let p = null;
|
|
255
|
+
if (scope === 'main') {
|
|
256
|
+
const memory = require('./memory');
|
|
257
|
+
p = path.join(memory.memDir(), 'MEMORY.md');
|
|
258
|
+
} else if (scope.startsWith('bot:')) {
|
|
259
|
+
const bots = require('./bots');
|
|
260
|
+
p = bots.memPath(scope.slice(4));
|
|
261
|
+
}
|
|
262
|
+
if (!p) return false;
|
|
263
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
264
|
+
fs.writeFileSync(p, body, 'utf8');
|
|
265
|
+
return true;
|
|
266
|
+
} catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* admin/UI MEMORY.md'yi elle değiştirdiyse store'u satırlardan yeniden kur */
|
|
272
|
+
function reindexFromLines(scope, lines) {
|
|
273
|
+
const items = (Array.isArray(lines) ? lines : [])
|
|
274
|
+
.map((t) => normText(t))
|
|
275
|
+
.filter(Boolean)
|
|
276
|
+
.map((text) => ({
|
|
277
|
+
id: crypto.randomUUID(),
|
|
278
|
+
text,
|
|
279
|
+
hash: md5(text),
|
|
280
|
+
vec: null,
|
|
281
|
+
created_at: new Date().toISOString(),
|
|
282
|
+
updated_at: new Date().toISOString(),
|
|
283
|
+
}));
|
|
284
|
+
const store = { version: 1, items };
|
|
285
|
+
saveStore(scope, store);
|
|
286
|
+
audit(scope, { event: 'IMPORT', count: items.length });
|
|
287
|
+
scheduleBackfill(scope);
|
|
288
|
+
return { ok: true, count: items.length };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ---------- arka plan vektör doldurma ---------- */
|
|
292
|
+
|
|
293
|
+
const _backfillRunning = new Set();
|
|
294
|
+
|
|
295
|
+
function scheduleBackfill(scope) {
|
|
296
|
+
if (_backfillRunning.has(scope)) return;
|
|
297
|
+
_backfillRunning.add(scope);
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
_backfill(scope)
|
|
300
|
+
.catch(() => {})
|
|
301
|
+
.finally(() => _backfillRunning.delete(scope));
|
|
302
|
+
}, 1200).unref();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function _backfill(scope) {
|
|
306
|
+
const fn = await getEmbedder();
|
|
307
|
+
if (!fn) return;
|
|
308
|
+
const store = loadStore(scope);
|
|
309
|
+
const todo = store.items.filter((it) => !it.vec).slice(0, 64);
|
|
310
|
+
if (!todo.length) return;
|
|
311
|
+
const texts = todo.map((it) => it.text);
|
|
312
|
+
const vecs = await embed(texts);
|
|
313
|
+
todo.forEach((it, i) => {
|
|
314
|
+
if (vecs[i]) it.vec = vecToB64(vecs[i]);
|
|
315
|
+
});
|
|
316
|
+
saveStore(scope, store);
|
|
317
|
+
if (store.items.some((it) => !it.vec)) scheduleBackfill(scope);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ---------- mem0 klasik konsolidasyon ---------- */
|
|
321
|
+
|
|
322
|
+
/* mem0 update prompt'u: yeni fact + benzer mevcut kayıtlar → tek LLM karar çağrısı.
|
|
323
|
+
ID'ler indeks ("0","1",...) — LLM'in UUID halüsinasyonuna karşı (mem0 anti-hallucination). */
|
|
324
|
+
function buildUpdatePrompt(facts, candidates) {
|
|
325
|
+
const sys =
|
|
326
|
+
'Sen bir uzun vadeli hafıza yöneticisisin. Yeni bilgi (fact) ile mevcut kayıtları karşılaştırıp ' +
|
|
327
|
+
'her fact için TEK karar ver. KURALLAR:\n' +
|
|
328
|
+
'1. ADD: hafızada olmayan yeni bilgi → {"event":"ADD","text":"<fact>"}\n' +
|
|
329
|
+
'2. UPDATE: mevcut kayıtla AYNI konu ama bilgi değişmiş/güncellenmiş → ' +
|
|
330
|
+
'{"event":"UPDATE","id":"<kayıt-no>","text":"<en kapsamlı güncel hali>"}\n' +
|
|
331
|
+
'3. DELETE: fact mevcut kayıtla ÇELİŞİYOR ve kayıt artık yanlışsa → {"event":"DELETE","id":"<kayıt-no>"}\n' +
|
|
332
|
+
'4. NONE: fact zaten mevcut kayıtta var (anlamca aynı) → {"event":"NONE"}\n' +
|
|
333
|
+
'Sadece kalıcı değeri olan bilgiler; geçici detay için NONE yerine ADD yapma — ' +
|
|
334
|
+
'gereksizse {"event":"DROP"} dön. Çıktı: SADECE JSON {"events":[...]} — fact sırasıyla.';
|
|
335
|
+
const candList = candidates
|
|
336
|
+
.map((c, i) => `{"id":"${i}","text":${JSON.stringify(c.item.text)}}`)
|
|
337
|
+
.join('\n');
|
|
338
|
+
const factList = facts.map((f, i) => `${i + 1}. ${f}`).join('\n');
|
|
339
|
+
const user =
|
|
340
|
+
'MEVCUT KAYITLAR:\n' + (candList || '(boş)') + '\n\n' +
|
|
341
|
+
'YENİ BİLGİLER:\n' + factList + '\n\n' +
|
|
342
|
+
'Her yeni bilgi için sırayla karar ver. SADECE JSON dön: {"events":[{"event":"ADD","text":"..."},{"event":"UPDATE","id":"3","text":"..."},{"event":"DELETE","id":"2"},{"event":"NONE"},{"event":"DROP"}]}';
|
|
343
|
+
return { sys, user };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseEvents(raw, candidateCount, factCount) {
|
|
347
|
+
try {
|
|
348
|
+
const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
|
|
349
|
+
const events = Array.isArray(parsed.events) ? parsed.events : [];
|
|
350
|
+
/* fact başına 1 karar kuralı; ama DELETE+ADD "değiştirme" ikilisi meşru — pay bırak */
|
|
351
|
+
return events.slice(0, factCount + 2).filter((e) => e && typeof e === 'object');
|
|
352
|
+
} catch {
|
|
353
|
+
return null; // parse edilemedi → fast-path fallback
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* idx ("3") → gerçek item id (anti-hallucination: harici eşleme) */
|
|
358
|
+
function resolveCandidate(candidates, idStr) {
|
|
359
|
+
const n = Number(String(idStr).replace(/[^0-9]/g, ''));
|
|
360
|
+
if (!Number.isInteger(n) || n < 0 || n >= candidates.length) return null;
|
|
361
|
+
return candidates[n];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* ---------- public API ---------- */
|
|
365
|
+
|
|
366
|
+
/** Kayıt ekleme (mem0 add). llm verilirse klasik konsolidasyon, yoksa hash+cos dedup. */
|
|
367
|
+
async function add(scope, texts, { llm } = {}) {
|
|
368
|
+
const list = (Array.isArray(texts) ? texts : [texts])
|
|
369
|
+
.map((t) => normText(t))
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.slice(0, 8);
|
|
372
|
+
if (!list.length) return { ok: false, error: 'empty' };
|
|
373
|
+
const store = loadStore(scope);
|
|
374
|
+
const newVecs = await embed(list);
|
|
375
|
+
|
|
376
|
+
const findCandidates = () => {
|
|
377
|
+
/* tüm yeni fact'lerin adaylarının birleşimi (mem0: per-fact retrieval → tek karar çağrısı) */
|
|
378
|
+
const best = new Map(); // item.id → { item, score }
|
|
379
|
+
for (const t of list) {
|
|
380
|
+
const tokens = tokenize(t);
|
|
381
|
+
for (let i = 0; i < list.length; i++) {
|
|
382
|
+
const qvec = newVecs[i];
|
|
383
|
+
for (const item of store.items) {
|
|
384
|
+
const ivec = b64ToVec(item.vec);
|
|
385
|
+
const sem = qvec && ivec ? cos(qvec, ivec) : 0;
|
|
386
|
+
const kw = scoreEntry(item.text, tokens);
|
|
387
|
+
const score = Math.max(sem, kw);
|
|
388
|
+
if (score < CAND_THRESHOLD) continue;
|
|
389
|
+
const prev = best.get(item.id);
|
|
390
|
+
if (!prev || prev.score < score) best.set(item.id, { item, score });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return [...best.values()].sort((a, b) => b.score - a.score).slice(0, CAND_K * list.length);
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const applyAdd = (text, vec) => {
|
|
398
|
+
const now = new Date().toISOString();
|
|
399
|
+
store.items.push({
|
|
400
|
+
id: crypto.randomUUID(),
|
|
401
|
+
text,
|
|
402
|
+
hash: md5(text),
|
|
403
|
+
vec: vec ? vecToB64(vec) : null,
|
|
404
|
+
created_at: now,
|
|
405
|
+
updated_at: now,
|
|
406
|
+
});
|
|
407
|
+
audit(scope, { event: 'ADD', text });
|
|
408
|
+
return { event: 'ADD', text };
|
|
409
|
+
};
|
|
410
|
+
const applyUpdate = (item, text, vec) => {
|
|
411
|
+
const old = item.text;
|
|
412
|
+
item.text = text;
|
|
413
|
+
item.hash = md5(text);
|
|
414
|
+
if (vec) item.vec = vecToB64(vec);
|
|
415
|
+
else item.vec = null;
|
|
416
|
+
item.updated_at = new Date().toISOString();
|
|
417
|
+
audit(scope, { event: 'UPDATE', id: item.id, old_text: old, text });
|
|
418
|
+
return { event: 'UPDATE', id: item.id, text };
|
|
419
|
+
};
|
|
420
|
+
const applyDelete = (item) => {
|
|
421
|
+
store.items = store.items.filter((it) => it.id !== item.id);
|
|
422
|
+
audit(scope, { event: 'DELETE', id: item.id, old_text: item.text });
|
|
423
|
+
return { event: 'DELETE', id: item.id };
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const applied = [];
|
|
427
|
+
let usedLlm = false;
|
|
428
|
+
|
|
429
|
+
if (typeof llm === 'function') {
|
|
430
|
+
try {
|
|
431
|
+
const candidates = findCandidates();
|
|
432
|
+
const { sys, user } = buildUpdatePrompt(list, candidates);
|
|
433
|
+
const raw = await llm(sys, user);
|
|
434
|
+
const events = parseEvents(raw, candidates.length, list.length);
|
|
435
|
+
if (events) {
|
|
436
|
+
usedLlm = true;
|
|
437
|
+
for (const ev of events) {
|
|
438
|
+
const evName = String(ev.event || '').toUpperCase();
|
|
439
|
+
const cand = ev.id != null ? resolveCandidate(candidates, ev.id) : null;
|
|
440
|
+
if (evName === 'ADD' && ev.text) {
|
|
441
|
+
const t = normText(ev.text);
|
|
442
|
+
if (store.items.some((it) => it.hash === md5(t))) continue;
|
|
443
|
+
applied.push(applyAdd(t, null)); /* vektör backfill'de */
|
|
444
|
+
} else if (evName === 'UPDATE' && cand && ev.text) {
|
|
445
|
+
applied.push(applyUpdate(cand.item, normText(ev.text), null));
|
|
446
|
+
} else if (evName === 'DELETE' && cand) {
|
|
447
|
+
applied.push(applyDelete(cand.item));
|
|
448
|
+
} else if (evName === 'NONE' || evName === 'DROP') {
|
|
449
|
+
applied.push({ event: evName === 'DROP' ? 'DROP' : 'NONE' });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch {}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!usedLlm) {
|
|
457
|
+
/* fast-path: hash dup → skip; SEMANTIC_SKIP üstü cosine → skip; yoksa ADD */
|
|
458
|
+
for (let i = 0; i < list.length; i++) {
|
|
459
|
+
const t = list[i];
|
|
460
|
+
const vec = newVecs[i];
|
|
461
|
+
if (store.items.some((it) => it.hash === md5(t))) {
|
|
462
|
+
applied.push({ event: 'NONE', text: t });
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
let twin = false;
|
|
466
|
+
if (vec) {
|
|
467
|
+
for (const it of store.items) {
|
|
468
|
+
const ivec = b64ToVec(it.vec);
|
|
469
|
+
if (ivec && cos(vec, ivec) >= SEMANTIC_SKIP) { twin = true; break; }
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (twin) {
|
|
473
|
+
applied.push({ event: 'NONE', text: t });
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
applied.push(applyAdd(t, vec));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* cap aşımı: en eskilerden düş */
|
|
481
|
+
if (store.items.length > CAP) {
|
|
482
|
+
const overflow = store.items.length - CAP;
|
|
483
|
+
const dropped = store.items.splice(0, overflow);
|
|
484
|
+
for (const d of dropped) audit(scope, { event: 'CAP_DROP', id: d.id, old_text: d.text });
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
saveStore(scope, store);
|
|
488
|
+
scheduleBackfill(scope);
|
|
489
|
+
return {
|
|
490
|
+
ok: true,
|
|
491
|
+
llm: usedLlm,
|
|
492
|
+
added: applied.filter((e) => e.event === 'ADD').length,
|
|
493
|
+
updated: applied.filter((e) => e.event === 'UPDATE').length,
|
|
494
|
+
deleted: applied.filter((e) => e.event === 'DELETE').length,
|
|
495
|
+
skipped: applied.filter((e) => e.event === 'NONE' || e.event === 'DROP').length,
|
|
496
|
+
events: applied,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Hibrit arama: 0.65*cosine + 0.35*keyword; vektör yoksa pure keyword (degradasyon). */
|
|
501
|
+
async function search(scope, query, { limit = 8, threshold = 0.05 } = {}) {
|
|
502
|
+
const q = String(query || '').trim();
|
|
503
|
+
const store = loadStore(scope);
|
|
504
|
+
if (!store.items.length || !q) return [];
|
|
505
|
+
const qTokens = tokenize(q);
|
|
506
|
+
const qvec = (await embed([q]))[0];
|
|
507
|
+
const rows = store.items.map((item) => {
|
|
508
|
+
const ivec = b64ToVec(item.vec);
|
|
509
|
+
const sem = qvec && ivec ? Math.max(0, cos(qvec, ivec)) : 0;
|
|
510
|
+
const kw = scoreEntry(item.text, qTokens);
|
|
511
|
+
const score = qvec && ivec
|
|
512
|
+
? SEARCH_SEM_W * sem + SEARCH_KW_W * kw
|
|
513
|
+
: kw;
|
|
514
|
+
return { id: item.id, text: item.text, score: Number(score.toFixed(3)), updated_at: item.updated_at };
|
|
515
|
+
});
|
|
516
|
+
return rows
|
|
517
|
+
.filter((r) => r.score > threshold)
|
|
518
|
+
.sort((a, b) => b.score - a.score)
|
|
519
|
+
.slice(0, limit);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Prompt için ilgili kayıtlar ('- ' satırları; memory.relevantFor uyumlu çıktı). */
|
|
523
|
+
async function relevant(scope, query, { maxRelevant = 6, maxRecent = 4, charCap = 2400 } = {}) {
|
|
524
|
+
const store = loadStore(scope);
|
|
525
|
+
if (!store.items.length) return '';
|
|
526
|
+
const picked = new Set();
|
|
527
|
+
try {
|
|
528
|
+
const hits = await search(scope, query, { limit: maxRelevant, threshold: 0.05 });
|
|
529
|
+
for (const h of hits) picked.add(h.id);
|
|
530
|
+
} catch {}
|
|
531
|
+
/* en yeniler de daima girer (memory.relevantFor davranışı) */
|
|
532
|
+
for (let i = store.items.length - 1; i >= 0 && picked.size < maxRelevant + maxRecent; i--) {
|
|
533
|
+
picked.add(store.items[i].id);
|
|
534
|
+
}
|
|
535
|
+
/* çıktı: insertion sırası korunur (ayna dosyasıyla tutarlı) */
|
|
536
|
+
let out = '';
|
|
537
|
+
for (const it of store.items) {
|
|
538
|
+
if (!picked.has(it.id)) continue;
|
|
539
|
+
const line = '- ' + it.text;
|
|
540
|
+
if (out.length + line.length > charCap) break;
|
|
541
|
+
out += (out ? '\n' : '') + line;
|
|
542
|
+
}
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function recentTexts(scope, n = 30) {
|
|
547
|
+
try {
|
|
548
|
+
return loadStore(scope).items.slice(-n).map((it) => it.text);
|
|
549
|
+
} catch {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function stats(scope) {
|
|
555
|
+
const store = loadStore(scope);
|
|
556
|
+
return {
|
|
557
|
+
scope: String(scope),
|
|
558
|
+
items: store.items.length,
|
|
559
|
+
withVec: store.items.filter((it) => it.vec).length,
|
|
560
|
+
cap: CAP,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/** mem0 FACT_RETRIEVAL (TR uyarlaması): sohbetten kalıcı fact'ler çıkarır. */
|
|
565
|
+
async function extractFacts(llm, transcript, existing, { max = 3 } = {}) {
|
|
566
|
+
if (typeof llm !== 'function') return [];
|
|
567
|
+
const sys =
|
|
568
|
+
'Aşağıdaki konuşma parçasından UZUN VADELİ hatırlanmaya değer bilgileri çıkar ' +
|
|
569
|
+
'(kullanıcının kalıcı tercihleri, projeleri/işleri, isimler, rutinler, düzeltmeler).\n' +
|
|
570
|
+
'KURALLAR:\n' +
|
|
571
|
+
`- Sadece haftalar sonra bile işe yarayacak şeyler; geçici detay asla\n` +
|
|
572
|
+
`- En fazla ${max} madde, her biri max 120 karakter, "kullanıcı ..." diye başla\n` +
|
|
573
|
+
'- ZATEN KAYITLI olanlarla anlamca aynıysa tekrar ekleme\n' +
|
|
574
|
+
'- Değerli bilgi yoksa boş dizi\n' +
|
|
575
|
+
'SADECE JSON dön: {"memories": ["...", "..."]} veya {"memories": []}';
|
|
576
|
+
const user =
|
|
577
|
+
'ZATEN KAYITLI olanlar (tekrar ekleme):\n' + (existing || '(boş)') + '\n\n' +
|
|
578
|
+
'# SOHBET\n' + transcript;
|
|
579
|
+
try {
|
|
580
|
+
const raw = await llm(sys, user);
|
|
581
|
+
const parsed = JSON.parse(String(raw || '').replace(/```(?:json)?|```/g, '').trim());
|
|
582
|
+
const arr = parsed && Array.isArray(parsed.memories) ? parsed.memories : [];
|
|
583
|
+
return arr.map((m) => normText(m)).filter(Boolean).slice(0, max);
|
|
584
|
+
} catch {
|
|
585
|
+
return [];
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
module.exports = {
|
|
590
|
+
add,
|
|
591
|
+
search,
|
|
592
|
+
relevant,
|
|
593
|
+
recentTexts,
|
|
594
|
+
extractFacts,
|
|
595
|
+
syncMirror,
|
|
596
|
+
reindexFromLines,
|
|
597
|
+
history,
|
|
598
|
+
stats,
|
|
599
|
+
setEmbedder,
|
|
600
|
+
mirrorLines,
|
|
601
|
+
CAP,
|
|
602
|
+
DIMS,
|
|
603
|
+
SEMANTIC_SKIP,
|
|
604
|
+
CAND_THRESHOLD,
|
|
605
|
+
};
|