|
|
|
|
@@ -83,9 +83,7 @@ class MemoryDB {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async store(
|
|
|
|
|
entry: Omit<MemoryEntry, "id" | "createdAt">,
|
|
|
|
|
): Promise<MemoryEntry> {
|
|
|
|
|
async store(entry: Omit<MemoryEntry, "id" | "createdAt">): Promise<MemoryEntry> {
|
|
|
|
|
await this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
const fullEntry: MemoryEntry = {
|
|
|
|
|
@@ -98,11 +96,7 @@ class MemoryDB {
|
|
|
|
|
return fullEntry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async search(
|
|
|
|
|
vector: number[],
|
|
|
|
|
limit = 5,
|
|
|
|
|
minScore = 0.5,
|
|
|
|
|
): Promise<MemorySearchResult[]> {
|
|
|
|
|
async search(vector: number[], limit = 5, minScore = 0.5): Promise<MemorySearchResult[]> {
|
|
|
|
|
await this.ensureInitialized();
|
|
|
|
|
|
|
|
|
|
const results = await this.table!.vectorSearch(vector).limit(limit).toArray();
|
|
|
|
|
@@ -131,8 +125,7 @@ class MemoryDB {
|
|
|
|
|
async delete(id: string): Promise<boolean> {
|
|
|
|
|
await this.ensureInitialized();
|
|
|
|
|
// Validate UUID format to prevent injection
|
|
|
|
|
const uuidRegex =
|
|
|
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
|
if (!uuidRegex.test(id)) {
|
|
|
|
|
throw new Error(`Invalid memory ID format: ${id}`);
|
|
|
|
|
}
|
|
|
|
|
@@ -203,8 +196,7 @@ function detectCategory(text: string): MemoryCategory {
|
|
|
|
|
const lower = text.toLowerCase();
|
|
|
|
|
if (/prefer|radši|like|love|hate|want/i.test(lower)) return "preference";
|
|
|
|
|
if (/rozhodli|decided|will use|budeme/i.test(lower)) return "decision";
|
|
|
|
|
if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower))
|
|
|
|
|
return "entity";
|
|
|
|
|
if (/\+\d{10,}|@[\w.-]+\.\w+|is called|jmenuje se/i.test(lower)) return "entity";
|
|
|
|
|
if (/is|are|has|have|je|má|jsou/i.test(lower)) return "fact";
|
|
|
|
|
return "other";
|
|
|
|
|
}
|
|
|
|
|
@@ -227,9 +219,7 @@ const memoryPlugin = {
|
|
|
|
|
const db = new MemoryDB(resolvedDbPath, vectorDim);
|
|
|
|
|
const embeddings = new Embeddings(cfg.embedding.apiKey, cfg.embedding.model!);
|
|
|
|
|
|
|
|
|
|
api.logger.info(
|
|
|
|
|
`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`,
|
|
|
|
|
);
|
|
|
|
|
api.logger.info(`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`);
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
|
// Tools
|
|
|
|
|
@@ -275,9 +265,7 @@ const memoryPlugin = {
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
content: [
|
|
|
|
|
{ type: "text", text: `Found ${results.length} memories:\n\n${text}` },
|
|
|
|
|
],
|
|
|
|
|
content: [{ type: "text", text: `Found ${results.length} memories:\n\n${text}` }],
|
|
|
|
|
details: { count: results.length, memories: sanitizedResults },
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
@@ -293,9 +281,7 @@ const memoryPlugin = {
|
|
|
|
|
"Save important information in long-term memory. Use for preferences, facts, decisions.",
|
|
|
|
|
parameters: Type.Object({
|
|
|
|
|
text: Type.String({ description: "Information to remember" }),
|
|
|
|
|
importance: Type.Optional(
|
|
|
|
|
Type.Number({ description: "Importance 0-1 (default: 0.7)" }),
|
|
|
|
|
),
|
|
|
|
|
importance: Type.Optional(Type.Number({ description: "Importance 0-1 (default: 0.7)" })),
|
|
|
|
|
category: Type.Optional(stringEnum(MEMORY_CATEGORIES)),
|
|
|
|
|
}),
|
|
|
|
|
async execute(_toolCallId, params) {
|
|
|
|
|
@@ -316,9 +302,16 @@ const memoryPlugin = {
|
|
|
|
|
if (existing.length > 0) {
|
|
|
|
|
return {
|
|
|
|
|
content: [
|
|
|
|
|
{ type: "text", text: `Similar memory already exists: "${existing[0].entry.text}"` },
|
|
|
|
|
{
|
|
|
|
|
type: "text",
|
|
|
|
|
text: `Similar memory already exists: "${existing[0].entry.text}"`,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
details: { action: "duplicate", existingId: existing[0].entry.id, existingText: existing[0].entry.text },
|
|
|
|
|
details: {
|
|
|
|
|
action: "duplicate",
|
|
|
|
|
existingId: existing[0].entry.id,
|
|
|
|
|
existingText: existing[0].entry.text,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -372,9 +365,7 @@ const memoryPlugin = {
|
|
|
|
|
if (results.length === 1 && results[0].score > 0.9) {
|
|
|
|
|
await db.delete(results[0].entry.id);
|
|
|
|
|
return {
|
|
|
|
|
content: [
|
|
|
|
|
{ type: "text", text: `Forgotten: "${results[0].entry.text}"` },
|
|
|
|
|
],
|
|
|
|
|
content: [{ type: "text", text: `Forgotten: "${results[0].entry.text}"` }],
|
|
|
|
|
details: { action: "deleted", id: results[0].entry.id },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
@@ -417,9 +408,7 @@ const memoryPlugin = {
|
|
|
|
|
|
|
|
|
|
api.registerCli(
|
|
|
|
|
({ program }) => {
|
|
|
|
|
const memory = program
|
|
|
|
|
.command("ltm")
|
|
|
|
|
.description("LanceDB memory plugin commands");
|
|
|
|
|
const memory = program.command("ltm").description("LanceDB memory plugin commands");
|
|
|
|
|
|
|
|
|
|
memory
|
|
|
|
|
.command("list")
|
|
|
|
|
@@ -478,9 +467,7 @@ const memoryPlugin = {
|
|
|
|
|
.map((r) => `- [${r.entry.category}] ${r.entry.text}`)
|
|
|
|
|
.join("\n");
|
|
|
|
|
|
|
|
|
|
api.logger.info?.(
|
|
|
|
|
`memory-lancedb: injecting ${results.length} memories into context`,
|
|
|
|
|
);
|
|
|
|
|
api.logger.info?.(`memory-lancedb: injecting ${results.length} memories into context`);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
prependContext: `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
|
|
|
|
|
@@ -536,9 +523,7 @@ const memoryPlugin = {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter for capturable content
|
|
|
|
|
const toCapture = texts.filter(
|
|
|
|
|
(text) => text && shouldCapture(text),
|
|
|
|
|
);
|
|
|
|
|
const toCapture = texts.filter((text) => text && shouldCapture(text));
|
|
|
|
|
if (toCapture.length === 0) return;
|
|
|
|
|
|
|
|
|
|
// Store each capturable piece (limit to 3 per conversation)
|
|
|
|
|
|