use crate::core::config::ConfigManager; use crate::llm::llm_models; use crate::shared::state::AppState; use log::{error, info, trace}; use std::collections::HashSet; use std::fmt::Write; use std::sync::Arc; use tokio::time::{interval, Duration}; use uuid::Uuid; pub fn start_episodic_memory_scheduler(state: Arc) { tokio::spawn(async move { tokio::time::sleep(Duration::from_secs(30)).await; let mut interval = interval(Duration::from_secs(60)); loop { interval.tick().await; if let Err(e) = process_episodic_memory(&Arc::clone(&state)).await { error!("Episodic memory processing failed: {}", e); } } }); } async fn process_episodic_memory( state: &Arc, ) -> Result<(), Box> { use scopeguard::guard; static SESSION_IN_PROGRESS: std::sync::LazyLock>> = std::sync::LazyLock::new(|| tokio::sync::Mutex::new(HashSet::new())); let sessions = { let mut session_manager = state.session_manager.lock().await; session_manager.get_user_sessions(Uuid::nil())? }; for session in sessions { let config_manager = ConfigManager::new(state.conn.clone()); let threshold = config_manager .get_config(&session.bot_id, "episodic-memory-threshold", None) .unwrap_or_default() .parse::() .unwrap_or(4); let history_to_keep = config_manager .get_config(&session.bot_id, "episodic-memory-history", None) .unwrap_or_default() .parse::() .unwrap_or(2); if threshold == 0 { return Ok(()); } else if threshold < 0 { trace!( "Negative episodic memory threshold detected for bot {}, skipping", session.bot_id ); continue; } let session_id = session.id; let history = { let mut session_manager = state.session_manager.lock().await; session_manager.get_conversation_history(session.id, session.user_id)? }; let mut messages_since_summary = 0; let mut has_new_messages = false; let last_summary_index = history .iter() .rev() .position(|(role, _)| role == "episodic" || role == "compact") .map(|pos| history.len() - pos - 1); let start_index = last_summary_index.map(|idx| idx + 1).unwrap_or(0); for (_i, (role, _)) in history.iter().enumerate().skip(start_index) { if role == "episodic" || role == "compact" { continue; } messages_since_summary += 1; has_new_messages = true; } if !has_new_messages && last_summary_index.is_some() { continue; } if messages_since_summary < threshold as usize { continue; } { let mut session_in_progress = SESSION_IN_PROGRESS.lock().await; if session_in_progress.contains(&session.id) { trace!( "Skipping session {} - episodic memory processing already in progress", session.id ); continue; } session_in_progress.insert(session.id); } trace!( "Creating episodic memory for session {}: {} messages since last summary (keeping last {})", session.id, messages_since_summary, history_to_keep ); let total_messages = history.len() - start_index; let messages_to_summarize = total_messages.saturating_sub(history_to_keep); if messages_to_summarize == 0 { trace!( "Not enough messages to create episodic memory for session {}", session.id ); continue; } let mut conversation = String::new(); conversation .push_str("Please summarize this conversation between user and bot: \n\n [[[***** \n"); for (role, content) in history.iter().skip(start_index).take(messages_to_summarize) { if role == "episodic" || role == "compact" { continue; } let _ = writeln!( conversation, "{}: {}", if role == "user" { "user" } else { "assistant" }, content ); } conversation.push_str("\n *****]]] \n Give me full points only, no explanations."); let messages = vec![serde_json::json!({ "role": "user", "content": conversation })]; let llm_provider = state.llm_provider.clone(); let mut filtered = String::new(); let config_manager = crate::core::config::ConfigManager::new(state.conn.clone()); let model = config_manager .get_config(&Uuid::nil(), "llm-model", None) .unwrap_or_default(); let key = config_manager .get_config(&Uuid::nil(), "llm-key", None) .unwrap_or_default(); let summarized = match llm_provider .generate("", &serde_json::Value::Array(messages), &model, &key) .await { Ok(summary) => { trace!( "Successfully created episodic memory for session {} ({} chars)", session.id, summary.len() ); let handler = llm_models::get_handler( config_manager .get_config(&session.bot_id, "llm-model", None) .unwrap_or_default() .as_str(), ); filtered = handler.process_content(&summary); format!("EPISODIC MEMORY: {}", filtered) } Err(e) => { error!( "Failed to create episodic memory for session {}: {}", session.id, e ); trace!("Using fallback summary for session {}", session.id); format!("EPISODIC MEMORY: {}", filtered) } }; info!( "Episodic memory created for session {}: {} messages summarized, {} kept", session.id, messages_to_summarize, history_to_keep ); { let mut session_manager = state.session_manager.lock().await; session_manager.save_message(session.id, session.user_id, 9, &summarized, 1)?; } let _session_cleanup = guard((), |_| { tokio::spawn(async move { let mut in_progress = SESSION_IN_PROGRESS.lock().await; in_progress.remove(&session_id); }); }); } Ok(()) }