//! Memory integration with mem-layer graph-based memory system use anyhow::{Context, Result};use serde::{Deserialize, Serialize};use std::process::Command; /// Memory node types#[derive(Debug, Clone, Serialize, Deserialize)]#[serde(rename_all = "lowercase")]pub enum NodeType { Bookmark, Page, Search, Conversation, Concept, Note,} /// Memory node#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MemoryNode { pub id: String, pub node_type: NodeType, pub content: String, pub tags: Vec<String>, pub metadata: serde_json::Value,} /// Memory relationship#[derive(Debug, Clone, Serialize, Deserialize)]pub struct MemoryRelation { pub from_id: String, pub to_id: String, pub rel_type: String,} /// Client for mem-layer CLIpub struct MemoryClient { scope: String, mem_layer_path: String, enabled: bool,} impl MemoryClient { /// Create a new memory client pub fn new(scope: String) -> Self { let mem_layer_path = which::which("mem-layer") .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| "mem-layer".to_string()); // Check if mem-layer is available let enabled = Command::new(&mem_layer_path) .arg("--version") .output() .map(|o| o.status.success()) .unwrap_or(false); Self { scope, mem_layer_path, enabled, } } /// Create from config pub fn from_config(config: &crate::config::Config) -> Option<Self> { let scope = config .memory_scope .clone() .unwrap_or_else(|| "azul-browse".to_string()); let client = Self::new(scope); if client.is_available() { Some(client) } else { None } } /// Check if mem-layer is available pub fn is_available(&self) -> bool { self.enabled } /// Add a note to memory pub fn add_note( &self, content: &str, tags: &[String], priority: Option<&str>, ) -> Result<String> { if !self.enabled { anyhow::bail!("mem-layer is not available"); } let mut cmd = Command::new(&self.mem_layer_path); cmd.arg("add") .arg("note") .arg(content) .arg("--scope") .arg(&self.scope); if !tags.is_empty() { cmd.arg("--tags").arg(tags.join(",")); } if let Some(p) = priority { cmd.arg("--priority").arg(p); } let output = cmd.output().context("Failed to execute mem-layer")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); anyhow::bail!("mem-layer add failed: {}", stderr); } let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout.trim().to_string()) } /// Search memory pub fn search(&self, query: &str) -> Result<Vec<String>> { if !self.enabled { anyhow::bail!("mem-layer is not available"); } let output = Command::new(&self.mem_layer_path) .arg("search") .arg(query) .arg("--scope") .arg(&self.scope) .output() .context("Failed to execute mem-layer")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); anyhow::bail!("mem-layer search failed: {}", stderr); } let stdout = String::from_utf8_lossy(&output.stdout); let results: Vec<String> = stdout .lines() .filter(|line| !line.is_empty()) .map(|s| s.to_string()) .collect(); Ok(results) } /// List nodes pub fn list_nodes(&self, node_type: Option<&str>, limit: Option<usize>) -> Result<Vec<String>> { if !self.enabled { anyhow::bail!("mem-layer is not available"); } let mut cmd = Command::new(&self.mem_layer_path); cmd.arg("list") .arg("--scope") .arg(&self.scope); if let Some(t) = node_type { cmd.arg("--type").arg(t); } if let Some(l) = limit { cmd.arg("--limit").arg(l.to_string()); } let output = cmd.output().context("Failed to execute mem-layer")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); anyhow::bail!("mem-layer list failed: {}", stderr); } let stdout = String::from_utf8_lossy(&output.stdout); // Parse the table output - skip header lines and decorations let nodes: Vec<String> = stdout .lines() .filter(|line| !line.is_empty()) .filter(|line| !line.starts_with("┏")) .filter(|line| !line.starts_with("┡")) .filter(|line| !line.starts_with("└")) .filter(|line| !line.starts_with("━")) .filter(|line| !line.contains("INFO - ")) .filter(|line| !line.trim_start().starts_with("ID")) .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) .collect(); Ok(nodes) } /// Store a browsing session pub fn store_page(&self, url: &str, title: &str, tags: &[String]) -> Result<String> { let content = format!("Page: {} ({})", title, url); self.add_note(&content, tags, Some("low")) } /// Store a chat conversation pub fn store_conversation(&self, query: &str, response: &str) -> Result<String> { let content = format!("Q: {}\nA: {}", query, response); self.add_note(&content, &["chat".to_string()], Some("medium")) } /// Get recent memory context pub fn get_recent_context(&self, limit: usize) -> Result<Vec<String>> { self.list_nodes(None, Some(limit)) }} #[cfg(test)]mod tests { use super::*; #[test] fn test_memory_client_creation() { let client = MemoryClient::new("test-scope".to_string()); // Should not panic even if mem-layer is not available assert!(!client.scope.is_empty()); }}
//! Memory integration with mem-layer graph-based memory system
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::process::Command;
/// Memory node types
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NodeType {
Bookmark,
Page,
Search,
Conversation,
Concept,
Note,
}
/// Memory node
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryNode {
pub id: String,
pub node_type: NodeType,
pub content: String,
pub tags: Vec<String>,
pub metadata: serde_json::Value,
}
/// Memory relationship
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryRelation {
pub from_id: String,
pub to_id: String,
pub rel_type: String,
}
/// Client for mem-layer CLI
pub struct MemoryClient {
scope: String,
mem_layer_path: String,
enabled: bool,
}
impl MemoryClient {
/// Create a new memory client
pub fn new(scope: String) -> Self {
let mem_layer_path = which::which("mem-layer")
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| "mem-layer".to_string());
// Check if mem-layer is available
let enabled = Command::new(&mem_layer_path)
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
Self {
scope,
mem_layer_path,
enabled,
}
}
/// Create from config
pub fn from_config(config: &crate::config::Config) -> Option<Self> {
let scope = config
.memory_scope
.clone()
.unwrap_or_else(|| "azul-browse".to_string());
let client = Self::new(scope);
if client.is_available() {
Some(client)
} else {
None
}
}
/// Check if mem-layer is available
pub fn is_available(&self) -> bool {
self.enabled
}
/// Add a note to memory
pub fn add_note(
&self,
content: &str,
tags: &[String],
priority: Option<&str>,
) -> Result<String> {
if !self.enabled {
anyhow::bail!("mem-layer is not available");
}
let mut cmd = Command::new(&self.mem_layer_path);
cmd.arg("add")
.arg("note")
.arg(content)
.arg("--scope")
.arg(&self.scope);
if !tags.is_empty() {
cmd.arg("--tags").arg(tags.join(","));
}
if let Some(p) = priority {
cmd.arg("--priority").arg(p);
}
let output = cmd.output().context("Failed to execute mem-layer")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("mem-layer add failed: {}", stderr);
}
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.trim().to_string())
}
/// Search memory
pub fn search(&self, query: &str) -> Result<Vec<String>> {
if !self.enabled {
anyhow::bail!("mem-layer is not available");
}
let output = Command::new(&self.mem_layer_path)
.arg("search")
.arg(query)
.arg("--scope")
.arg(&self.scope)
.output()
.context("Failed to execute mem-layer")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("mem-layer search failed: {}", stderr);
}
let stdout = String::from_utf8_lossy(&output.stdout);
let results: Vec<String> = stdout
.lines()
.filter(|line| !line.is_empty())
.map(|s| s.to_string())
.collect();
Ok(results)
}
/// List nodes
pub fn list_nodes(&self, node_type: Option<&str>, limit: Option<usize>) -> Result<Vec<String>> {
if !self.enabled {
anyhow::bail!("mem-layer is not available");
}
let mut cmd = Command::new(&self.mem_layer_path);
cmd.arg("list")
.arg("--scope")
.arg(&self.scope);
if let Some(t) = node_type {
cmd.arg("--type").arg(t);
}
if let Some(l) = limit {
cmd.arg("--limit").arg(l.to_string());
}
let output = cmd.output().context("Failed to execute mem-layer")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("mem-layer list failed: {}", stderr);
}
let stdout = String::from_utf8_lossy(&output.stdout);
// Parse the table output - skip header lines and decorations
let nodes: Vec<String> = stdout
.lines()
.filter(|line| !line.is_empty())
.filter(|line| !line.starts_with("┏"))
.filter(|line| !line.starts_with("┡"))
.filter(|line| !line.starts_with("└"))
.filter(|line| !line.starts_with("━"))
.filter(|line| !line.contains("INFO - "))
.filter(|line| !line.trim_start().starts_with("ID"))
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Ok(nodes)
}
/// Store a browsing session
pub fn store_page(&self, url: &str, title: &str, tags: &[String]) -> Result<String> {
let content = format!("Page: {} ({})", title, url);
self.add_note(&content, tags, Some("low"))
}
/// Store a chat conversation
pub fn store_conversation(&self, query: &str, response: &str) -> Result<String> {
let content = format!("Q: {}\nA: {}", query, response);
self.add_note(&content, &["chat".to_string()], Some("medium"))
}
/// Get recent memory context
pub fn get_recent_context(&self, limit: usize) -> Result<Vec<String>> {
self.list_nodes(None, Some(limit))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_client_creation() {
let client = MemoryClient::new("test-scope".to_string());
// Should not panic even if mem-layer is not available
assert!(!client.scope.is_empty());
}
}