use std::collections::HashMap;
use std::path::Path;
use thiserror::Error;
use crate::{
config::Config,
db::{DbError, PalaceDb},
embedder::Embedder,
search::{SearchError, Searcher},
types::Drawer,
};
#[derive(Debug, Error)]
pub enum LayerError {
#[error("db: {0}")]
Db(#[from] DbError),
#[error("search: {0}")]
Search(#[from] SearchError),
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
pub async fn l0_render(identity_path: &Path) -> String {
if identity_path.exists() {
match tokio::fs::read_to_string(identity_path).await {
Ok(text) => return text.trim().to_string(),
Err(_) => {}
}
}
"## L0 — IDENTITY\nNo identity configured. Create ~/.aimem/identity.txt".to_string()
}
const L1_MAX_DRAWERS: usize = 15;
const L1_MAX_CHARS: usize = 3200;
pub async fn l1_generate(db: &PalaceDb, wing: Option<&str>) -> Result<String, LayerError> {
let drawers = db.fetch_drawers(wing, None, 200).await?;
if drawers.is_empty() {
return Ok("## L1 — No memories yet.".to_string());
}
let mut by_room: HashMap<String, Vec<Drawer>> = HashMap::new();
for d in drawers.into_iter().take(L1_MAX_DRAWERS) {
by_room.entry(d.room.clone()).or_default().push(d);
}
let mut lines = vec!["## L1 — ESSENTIAL STORY".to_string()];
let mut total_len = 0usize;
let mut rooms: Vec<_> = by_room.iter().collect();
rooms.sort_by_key(|(r, _)| r.as_str());
for (room, entries) in rooms {
let room_line = format!("\n[{}]", room);
lines.push(room_line.clone());
total_len += room_line.len();
for d in entries {
let snippet: String = d
.content
.trim()
.replace('\n', " ")
.chars()
.take(200)
.collect();
let snippet = if d.content.len() > 200 {
format!("{}...", snippet)
} else {
snippet
};
let source_tag = d
.source_file
.as_deref()
.and_then(|s| Path::new(s).file_name())
.map(|n| format!(" ({})", n.to_string_lossy()))
.unwrap_or_default();
let entry = format!(" - {}{}", snippet, source_tag);
if total_len + entry.len() > L1_MAX_CHARS {
lines.push(" ... (more in L3 search)".to_string());
return Ok(lines.join("\n"));
}
total_len += entry.len();
lines.push(entry);
}
}
Ok(lines.join("\n"))
}
pub async fn l2_retrieve(
db: &PalaceDb,
wing: Option<&str>,
room: Option<&str>,
n: usize,
) -> Result<String, LayerError> {
let drawers = db.fetch_drawers(wing, room, n).await?;
if drawers.is_empty() {
let label = match (wing, room) {
(Some(w), Some(r)) => format!("wing={w} room={r}"),
(Some(w), None) => format!("wing={w}"),
(None, Some(r)) => format!("room={r}"),
(None, None) => "all".to_string(),
};
return Ok(format!("No drawers found for {label}."));
}
let mut lines = vec![format!("## L2 — ON-DEMAND ({} drawers)", drawers.len())];
for d in &drawers {
let snippet: String = d
.content
.trim()
.replace('\n', " ")
.chars()
.take(300)
.collect();
let snippet = if d.content.len() > 300 {
format!("{}...", snippet)
} else {
snippet
};
let source_tag = d
.source_file
.as_deref()
.and_then(|s| Path::new(s).file_name())
.map(|n| format!(" ({})", n.to_string_lossy()))
.unwrap_or_default();
lines.push(format!(" [{}] {}{}", d.room, snippet, source_tag));
}
Ok(lines.join("\n"))
}
pub async fn l3_search(
searcher: &Searcher,
query: &str,
wing: Option<&str>,
room: Option<&str>,
n: usize,
) -> Result<String, LayerError> {
let results = searcher.vector_search(query, wing, room, n).await?;
if results.is_empty() {
return Ok("No results found.".to_string());
}
let mut lines = vec![format!("## L3 — SEARCH RESULTS for \"{}\"", query)];
for (i, r) in results.iter().enumerate() {
let snippet: String = r
.drawer
.content
.trim()
.replace('\n', " ")
.chars()
.take(300)
.collect();
let snippet = if r.drawer.content.len() > 300 {
format!("{}...", snippet)
} else {
snippet
};
let source = r
.drawer
.source_file
.as_deref()
.and_then(|s| Path::new(s).file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
lines.push(format!(
" [{}] {}/{} (sim={:.3})",
i + 1,
r.drawer.wing,
r.drawer.room,
r.similarity,
));
lines.push(format!(" {}", snippet));
if !source.is_empty() {
lines.push(format!(" src: {}", source));
}
}
Ok(lines.join("\n"))
}
#[derive(Debug, Clone)]
pub struct MemoryStack {
db: PalaceDb,
searcher: Searcher,
identity_path: std::path::PathBuf,
}
impl MemoryStack {
pub fn new(db: PalaceDb, embedder: Embedder, cfg: &Config) -> Self {
let searcher = Searcher::new(db.clone(), embedder);
Self {
db,
searcher,
identity_path: cfg.identity_path.clone(),
}
}
pub async fn wake_up(&self, wing: Option<&str>) -> Result<String, LayerError> {
let l0 = l0_render(&self.identity_path).await;
let l1 = l1_generate(&self.db, wing).await?;
Ok(format!("{}\n\n{}", l0, l1))
}
pub async fn recall(
&self,
wing: Option<&str>,
room: Option<&str>,
) -> Result<String, LayerError> {
l2_retrieve(&self.db, wing, room, 10).await
}
pub async fn search(
&self,
query: &str,
wing: Option<&str>,
room: Option<&str>,
) -> Result<String, LayerError> {
l3_search(&self.searcher, query, wing, room, 5).await
}
pub async fn status(&self) -> Result<serde_json::Value, LayerError> {
let count = self.db.drawer_count().await?;
let (wings, rooms) = self.db.taxonomy().await?;
let l0_exists = self.identity_path.exists();
let l0_tokens = if l0_exists {
std::fs::read_to_string(&self.identity_path)
.map(|s| s.len() / 4)
.unwrap_or(0)
} else {
0
};
Ok(serde_json::json!({
"total_drawers": count,
"identity_path": self.identity_path.display().to_string(),
"identity_exists": l0_exists,
"identity_tokens_est": l0_tokens,
"wings": wings.into_iter().map(|(w, c)| serde_json::json!({"wing": w, "count": c})).collect::<Vec<_>>(),
"rooms": rooms.into_iter().map(|(r, c)| serde_json::json!({"room": r, "count": c})).collect::<Vec<_>>(),
}))
}
}