zeroclaw/src/memory/backend.rs
khhjoe 00209dd899
feat(memory): add mem0 (OpenMemory) backend integration (#3965)
* feat(memory): add mem0 (OpenMemory) backend integration

- Implement Mem0Memory struct with full Memory trait
- Add history() audit trail, recall_filtered() with time/metadata filters
- Add store_procedural() for conversation trace extraction
- Add ProceduralMessage type to Memory trait with default no-op
- Feature-gated behind `memory-mem0` flag
- 9 unit tests covering edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(memory): add extraction_prompt config, deploy scripts, and timing instrumentation

- Add `extraction_prompt` field to `Mem0Config` for custom LLM fact
  extraction prompts (e.g. Cantonese/Chinese content), with
  `MEM0_EXTRACTION_PROMPT` env var fallback
- Pass `custom_instructions` in mem0 store requests so the server
  uses the client-supplied prompt over its default
- Add timing instrumentation to channel message pipeline
  (mem_recall_ms, elapsed_before_llm_ms, llm_call_ms, total_ms)
- Add `deploy/mem0/` with self-hosted mem0 + reranker GPU server
  scripts, fully configurable via environment variables
- Update config reference docs (EN, zh-CN, VI) with `[memory.mem0]`
  subsection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

# Conflicts:
#	src/channels/mod.rs

* chore: remove accidentally staged worktree from index

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 18:22:44 -04:00

187 lines
5.6 KiB
Rust

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum MemoryBackendKind {
Sqlite,
Lucid,
Postgres,
Qdrant,
Mem0,
Markdown,
None,
Unknown,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct MemoryBackendProfile {
pub key: &'static str,
pub label: &'static str,
pub auto_save_default: bool,
pub uses_sqlite_hygiene: bool,
pub sqlite_based: bool,
pub optional_dependency: bool,
}
const SQLITE_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "sqlite",
label: "SQLite with Vector Search (recommended) — fast, hybrid search, embeddings",
auto_save_default: true,
uses_sqlite_hygiene: true,
sqlite_based: true,
optional_dependency: false,
};
const LUCID_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "lucid",
label: "Lucid Memory bridge — sync with local lucid-memory CLI, keep SQLite fallback",
auto_save_default: true,
uses_sqlite_hygiene: true,
sqlite_based: true,
optional_dependency: true,
};
const MARKDOWN_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "markdown",
label: "Markdown Files — simple, human-readable, no dependencies",
auto_save_default: true,
uses_sqlite_hygiene: false,
sqlite_based: false,
optional_dependency: false,
};
const POSTGRES_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "postgres",
label: "PostgreSQL — remote durable storage via [storage.provider.config]",
auto_save_default: true,
uses_sqlite_hygiene: false,
sqlite_based: false,
optional_dependency: true,
};
const QDRANT_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "qdrant",
label: "Qdrant — vector database for semantic search via [memory.qdrant]",
auto_save_default: true,
uses_sqlite_hygiene: false,
sqlite_based: false,
optional_dependency: false,
};
const MEM0_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "mem0",
label: "Mem0 (OpenMemory) — semantic memory with LLM fact extraction via [memory.mem0]",
auto_save_default: true,
uses_sqlite_hygiene: false,
sqlite_based: false,
optional_dependency: true,
};
const NONE_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "none",
label: "None — disable persistent memory",
auto_save_default: false,
uses_sqlite_hygiene: false,
sqlite_based: false,
optional_dependency: false,
};
const CUSTOM_PROFILE: MemoryBackendProfile = MemoryBackendProfile {
key: "custom",
label: "Custom backend — extension point",
auto_save_default: true,
uses_sqlite_hygiene: false,
sqlite_based: false,
optional_dependency: false,
};
const SELECTABLE_MEMORY_BACKENDS: [MemoryBackendProfile; 4] = [
SQLITE_PROFILE,
LUCID_PROFILE,
MARKDOWN_PROFILE,
NONE_PROFILE,
];
pub fn selectable_memory_backends() -> &'static [MemoryBackendProfile] {
&SELECTABLE_MEMORY_BACKENDS
}
pub fn default_memory_backend_key() -> &'static str {
SQLITE_PROFILE.key
}
pub fn classify_memory_backend(backend: &str) -> MemoryBackendKind {
match backend {
"sqlite" => MemoryBackendKind::Sqlite,
"lucid" => MemoryBackendKind::Lucid,
"postgres" => MemoryBackendKind::Postgres,
"qdrant" => MemoryBackendKind::Qdrant,
"mem0" | "openmemory" => MemoryBackendKind::Mem0,
"markdown" => MemoryBackendKind::Markdown,
"none" => MemoryBackendKind::None,
_ => MemoryBackendKind::Unknown,
}
}
pub fn memory_backend_profile(backend: &str) -> MemoryBackendProfile {
match classify_memory_backend(backend) {
MemoryBackendKind::Sqlite => SQLITE_PROFILE,
MemoryBackendKind::Lucid => LUCID_PROFILE,
MemoryBackendKind::Postgres => POSTGRES_PROFILE,
MemoryBackendKind::Qdrant => QDRANT_PROFILE,
MemoryBackendKind::Mem0 => MEM0_PROFILE,
MemoryBackendKind::Markdown => MARKDOWN_PROFILE,
MemoryBackendKind::None => NONE_PROFILE,
MemoryBackendKind::Unknown => CUSTOM_PROFILE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_known_backends() {
assert_eq!(classify_memory_backend("sqlite"), MemoryBackendKind::Sqlite);
assert_eq!(classify_memory_backend("lucid"), MemoryBackendKind::Lucid);
assert_eq!(
classify_memory_backend("postgres"),
MemoryBackendKind::Postgres
);
assert_eq!(
classify_memory_backend("markdown"),
MemoryBackendKind::Markdown
);
assert_eq!(classify_memory_backend("none"), MemoryBackendKind::None);
}
#[test]
fn classify_unknown_backend() {
assert_eq!(classify_memory_backend("redis"), MemoryBackendKind::Unknown);
}
#[test]
fn selectable_backends_are_ordered_for_onboarding() {
let backends = selectable_memory_backends();
assert_eq!(backends.len(), 4);
assert_eq!(backends[0].key, "sqlite");
assert_eq!(backends[1].key, "lucid");
assert_eq!(backends[2].key, "markdown");
assert_eq!(backends[3].key, "none");
}
#[test]
fn lucid_profile_is_sqlite_based_optional_backend() {
let profile = memory_backend_profile("lucid");
assert!(profile.sqlite_based);
assert!(profile.optional_dependency);
assert!(profile.uses_sqlite_hygiene);
}
#[test]
fn unknown_profile_preserves_extensibility_defaults() {
let profile = memory_backend_profile("custom-memory");
assert_eq!(profile.key, "custom");
assert!(profile.auto_save_default);
assert!(!profile.uses_sqlite_hygiene);
}
}