extensions: add memory-powermem plugin for long-term memory
This commit is contained in:
parent
6e09492a37
commit
fd7999cefa
@ -21,25 +21,33 @@ Moltbot long-term memory plugin backed by [PowerMem](https://github.com/oceanbas
|
|||||||
|
|
||||||
## Moltbot configuration
|
## Moltbot configuration
|
||||||
|
|
||||||
1. Set the memory slot to this plugin:
|
1. Enable the plugin and set the memory slot to this plugin. Edit your Moltbot config (e.g. `~/.clawdbot/config.json` or `moltbot.json` in your state dir):
|
||||||
|
|
||||||
```yaml
|
```json
|
||||||
plugins:
|
{
|
||||||
slots:
|
"plugins": {
|
||||||
memory: memory-powermem
|
"slots": { "memory": "memory-powermem" },
|
||||||
config:
|
"entries": {
|
||||||
memory-powermem:
|
"memory-powermem": {
|
||||||
baseUrl: "http://localhost:8000"
|
"enabled": true,
|
||||||
# apiKey: "optional-if-auth-enabled"
|
"config": {
|
||||||
autoCapture: true
|
"baseUrl": "http://localhost:8000",
|
||||||
autoRecall: true
|
"autoCapture": true,
|
||||||
inferOnAdd: true
|
"autoRecall": true,
|
||||||
# userId: "optional-override"
|
"inferOnAdd": true
|
||||||
# agentId: "optional-override"
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional: `apiKey` (if PowerMem server has auth), `userId`, `agentId` — see Options below.
|
||||||
|
|
||||||
2. Ensure PowerMem server is running before starting the gateway.
|
2. Ensure PowerMem server is running before starting the gateway.
|
||||||
|
|
||||||
|
**Auto-capture:** When a conversation ends, the full user/assistant text is sent to PowerMem with `infer: true`; PowerMem extracts and stores memories (no trigger phrases) (e.g. “remember …”, “I like …”, “important”, or Chinese “记得/记住/我喜欢/重要/偏好”). At most 3 chunks per session (each up to 6000 chars). To test: run `moltbot ltm health`, then `moltbot ltm add "User prefers dark mode"` and `moltbot ltm search "dark mode"`. In chat, ask the agent to “remember that I prefer tea” or say “我喜欢用 Python” so a message is auto-captured.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Option | Required | Description |
|
| Option | Required | Description |
|
||||||
@ -62,6 +70,7 @@ Moltbot long-term memory plugin backed by [PowerMem](https://github.com/oceanbas
|
|||||||
|
|
||||||
- `moltbot ltm search <query> [--limit n]` — Search memories.
|
- `moltbot ltm search <query> [--limit n]` — Search memories.
|
||||||
- `moltbot ltm health` — Check PowerMem server health.
|
- `moltbot ltm health` — Check PowerMem server health.
|
||||||
|
- `moltbot ltm add "<text>"` — Manually store one memory (for testing or one-off storage).
|
||||||
|
|
||||||
## Running tests
|
## Running tests
|
||||||
|
|
||||||
|
|||||||
@ -17,32 +17,6 @@ import {
|
|||||||
} from "./config.js";
|
} from "./config.js";
|
||||||
import { PowerMemClient } from "./client.js";
|
import { PowerMemClient } from "./client.js";
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Rule-based capture filter (same idea as memory-lancedb)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
const MEMORY_TRIGGERS = [
|
|
||||||
/zapamatuj si|pamatuj|remember/i,
|
|
||||||
/preferuji|radši|nechci|prefer/i,
|
|
||||||
/rozhodli jsme|budeme používat/i,
|
|
||||||
/\+\d{10,}/,
|
|
||||||
/[\w.-]+@[\w.-]+\.\w+/,
|
|
||||||
/můj\s+\w+\s+je|je\s+můj/i,
|
|
||||||
/my\s+\w+\s+is|is\s+my/i,
|
|
||||||
/i (like|prefer|hate|love|want|need)/i,
|
|
||||||
/always|never|important/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
function shouldCapture(text: string): boolean {
|
|
||||||
if (text.length < 10 || text.length > 500) return false;
|
|
||||||
if (text.includes("<relevant-memories>")) return false;
|
|
||||||
if (text.startsWith("<") && text.includes("</")) return false;
|
|
||||||
if (text.includes("**") && text.includes("\n-")) return false;
|
|
||||||
const emojiCount = (text.match(/[\u{1F300}-\u{1F9FF}]/gu) || []).length;
|
|
||||||
if (emojiCount > 3) return false;
|
|
||||||
return MEMORY_TRIGGERS.some((r) => r.test(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Plugin Definition
|
// Plugin Definition
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -309,6 +283,24 @@ const memoryPlugin = {
|
|||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ltm
|
||||||
|
.command("add")
|
||||||
|
.description("Manually add a memory (for testing or one-off storage)")
|
||||||
|
.argument("<text>", "Content to store")
|
||||||
|
.action(async (text: string) => {
|
||||||
|
try {
|
||||||
|
const created = await client.add(text.trim(), { infer: cfg.inferOnAdd });
|
||||||
|
if (created.length === 0) {
|
||||||
|
console.log("Stored (no inferred items).");
|
||||||
|
} else {
|
||||||
|
console.log(`Stored ${created.length} item(s):`, created.map((c) => c.memory_id));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("PowerMem add failed:", err);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
{ commands: ["ltm"] },
|
{ commands: ["ltm"] },
|
||||||
);
|
);
|
||||||
@ -372,16 +364,34 @@ const memoryPlugin = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const toCapture = texts.filter((t) => t && shouldCapture(t));
|
// Use all conversation content; PowerMem infers memories (no trigger-phrase filter)
|
||||||
if (toCapture.length === 0) return;
|
const MIN_LEN = 10;
|
||||||
|
const MAX_CHUNK_LEN = 6000;
|
||||||
|
const MAX_CHUNKS_PER_SESSION = 3;
|
||||||
|
const sanitized = texts
|
||||||
|
.filter((t): t is string => typeof t === "string" && t.trim().length >= MIN_LEN)
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(
|
||||||
|
(t) =>
|
||||||
|
!t.includes("<relevant-memories>") &&
|
||||||
|
!(t.startsWith("<") && t.includes("</")),
|
||||||
|
);
|
||||||
|
if (sanitized.length === 0) return;
|
||||||
|
|
||||||
|
const combined = sanitized.join("\n\n");
|
||||||
|
const chunks: string[] = [];
|
||||||
|
for (let i = 0; i < combined.length; i += MAX_CHUNK_LEN) {
|
||||||
|
if (chunks.length >= MAX_CHUNKS_PER_SESSION) break;
|
||||||
|
chunks.push(combined.slice(i, i + MAX_CHUNK_LEN));
|
||||||
|
}
|
||||||
|
|
||||||
let stored = 0;
|
let stored = 0;
|
||||||
for (const text of toCapture.slice(0, 3)) {
|
for (const chunk of chunks) {
|
||||||
const created = await client.add(text, { infer: cfg.inferOnAdd });
|
const created = await client.add(chunk, { infer: cfg.inferOnAdd });
|
||||||
stored += created.length;
|
stored += created.length;
|
||||||
}
|
}
|
||||||
if (stored > 0) {
|
if (stored > 0) {
|
||||||
api.logger.info?.(`memory-powermem: auto-captured ${stored} memories`);
|
api.logger.info?.(`memory-powermem: auto-captured ${stored} memories from conversation`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
api.logger.warn?.(`memory-powermem: capture failed: ${String(err)}`);
|
api.logger.warn?.(`memory-powermem: capture failed: ${String(err)}`);
|
||||||
|
|||||||
1
extensions/memory-powermem/node_modules/@sinclair/typebox
generated
vendored
1
extensions/memory-powermem/node_modules/@sinclair/typebox
generated
vendored
@ -1 +0,0 @@
|
|||||||
../../../../node_modules/.pnpm/@sinclair+typebox@0.34.47/node_modules/@sinclair/typebox
|
|
||||||
Loading…
Reference in New Issue
Block a user