198 lines
6.4 KiB
JavaScript
198 lines
6.4 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const {Environment} = await import("../dist/common/environment.js");
|
|
const {
|
|
buildUserMemoryPrompt,
|
|
compressMemoryWithFallback,
|
|
deleteUserMemory,
|
|
getMemoryFilePath,
|
|
readUserMemory,
|
|
updateUserMemory,
|
|
} = await import("../dist/ai/tools/user-memory.js");
|
|
const {AiProvider} = await import("../dist/model/ai-provider.js");
|
|
|
|
function makeTempDataPath() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), "tg-chat-bot-memory-"));
|
|
}
|
|
|
|
function withEnv(vars, fn) {
|
|
const snapshot = new Map();
|
|
for (const [key, value] of Object.entries(vars)) {
|
|
snapshot.set(key, process.env[key]);
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
|
|
return Promise.resolve(fn()).finally(() => {
|
|
for (const [key, value] of snapshot.entries()) {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
} else {
|
|
process.env[key] = value;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
test("memory storage supports append replace and remove", async () => {
|
|
const oldDataPath = Environment.DATA_PATH;
|
|
Environment.DATA_PATH = makeTempDataPath();
|
|
|
|
try {
|
|
const userId = 475823381;
|
|
|
|
let result = await updateUserMemory({
|
|
userId,
|
|
scope: "user",
|
|
action: "replace",
|
|
content: "# Profile\nLikes tea",
|
|
});
|
|
assert.equal(result.success, true);
|
|
|
|
result = await updateUserMemory({
|
|
userId,
|
|
scope: "user",
|
|
action: "add",
|
|
content: "Prefers concise answers",
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.match(result.content, /Likes tea/);
|
|
assert.match(result.content, /Prefers concise answers/);
|
|
|
|
result = await updateUserMemory({
|
|
userId,
|
|
scope: "user",
|
|
action: "remove",
|
|
content: "Prefers concise answers",
|
|
});
|
|
assert.equal(result.success, true);
|
|
assert.doesNotMatch(result.content, /Prefers concise answers/);
|
|
|
|
const readback = await readUserMemory(userId, "user");
|
|
assert.equal(readback.success, true);
|
|
assert.equal(readback.filePath, getMemoryFilePath(userId, "user"));
|
|
assert.match(readback.content, /Likes tea/);
|
|
} finally {
|
|
Environment.DATA_PATH = oldDataPath;
|
|
}
|
|
});
|
|
|
|
test("memory delete removes the file", async () => {
|
|
const oldDataPath = Environment.DATA_PATH;
|
|
Environment.DATA_PATH = makeTempDataPath();
|
|
|
|
try {
|
|
const userId = 999;
|
|
await updateUserMemory({userId, scope: "user", action: "replace", content: "hello"});
|
|
const deleted = await deleteUserMemory(userId, "user");
|
|
assert.equal(deleted.success, true);
|
|
const readback = await readUserMemory(userId, "user");
|
|
assert.equal(readback.success, true);
|
|
assert.equal(readback.content, "");
|
|
} finally {
|
|
Environment.DATA_PATH = oldDataPath;
|
|
}
|
|
});
|
|
|
|
test("memory prompt combines system and user files", async () => {
|
|
const oldDataPath = Environment.DATA_PATH;
|
|
Environment.DATA_PATH = makeTempDataPath();
|
|
|
|
try {
|
|
const userId = 1234;
|
|
|
|
await updateUserMemory({
|
|
userId,
|
|
scope: "system",
|
|
action: "replace",
|
|
content: "Ты зовешься Евлампий.",
|
|
});
|
|
await updateUserMemory({
|
|
userId,
|
|
scope: "user",
|
|
action: "replace",
|
|
content: "Пользователь любит короткие ответы.",
|
|
});
|
|
|
|
const prompt = await buildUserMemoryPrompt(userId);
|
|
assert(prompt);
|
|
assert.equal(prompt?.includes("## Assistant memory (system.md)"), true);
|
|
assert.equal(prompt?.includes("This is information about the assistant and its behavior."), true);
|
|
assert.equal(prompt?.includes("## User memory (user.md)"), true);
|
|
assert.equal(prompt?.includes("This is information about the user."), true);
|
|
assert(prompt.indexOf("## Assistant memory (system.md)") < prompt.indexOf("## User memory (user.md)"));
|
|
} finally {
|
|
Environment.DATA_PATH = oldDataPath;
|
|
}
|
|
});
|
|
|
|
test("memory compression falls back to current target when explicit target fails", async () => {
|
|
await withEnv({
|
|
OLLAMA_MEMORY_COMPRESS_MODEL: "memory-compress-model",
|
|
OLLAMA_CHAT_MODEL: "chat-model",
|
|
}, async () => {
|
|
const calls = [];
|
|
const result = await compressMemoryWithFallback(
|
|
{
|
|
provider: AiProvider.OLLAMA,
|
|
currentTarget: {
|
|
provider: AiProvider.OLLAMA,
|
|
purpose: "chat",
|
|
model: "chat-model",
|
|
},
|
|
scope: "system",
|
|
currentText: "x".repeat(1200),
|
|
limit: 1000,
|
|
},
|
|
async ({target}) => {
|
|
calls.push(target.model);
|
|
if (target.model === "memory-compress-model") {
|
|
throw new Error("boom");
|
|
}
|
|
return "short summary";
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["memory-compress-model", "chat-model"]);
|
|
assert.equal(result.content, "short summary");
|
|
assert.equal(result.compressed, true);
|
|
});
|
|
});
|
|
|
|
test("memory compression uses current target when no separate target exists", async () => {
|
|
await withEnv({
|
|
OLLAMA_MEMORY_COMPRESS_MODEL: undefined,
|
|
OLLAMA_CHAT_MODEL: "chat-model",
|
|
}, async () => {
|
|
const calls = [];
|
|
const result = await compressMemoryWithFallback(
|
|
{
|
|
provider: AiProvider.OLLAMA,
|
|
currentTarget: {
|
|
provider: AiProvider.OLLAMA,
|
|
purpose: "chat",
|
|
model: "chat-model",
|
|
},
|
|
scope: "user",
|
|
currentText: "x".repeat(1200),
|
|
limit: 1000,
|
|
},
|
|
async ({target}) => {
|
|
calls.push(target.model);
|
|
return "summary";
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(calls, ["chat-model"]);
|
|
assert.equal(result.content, "summary");
|
|
assert.equal(result.compressed, true);
|
|
});
|
|
});
|