//! Summarizer Adapter - Bridges zclaw_growth::SummaryLlmDriver with Tauri LLM Client //! //! Implements the SummaryLlmDriver trait using the local LlmClient, //! enabling L0/L1 summary generation via the user's configured LLM. use zclaw_growth::{MemoryEntry, SummaryLlmDriver, summarizer::{overview_prompt, abstract_prompt}}; /// Tauri-side implementation of SummaryLlmDriver using llm::LlmClient pub struct TauriSummaryDriver { endpoint: String, api_key: String, model: Option, } impl TauriSummaryDriver { /// Create a new Tauri summary driver pub fn new(endpoint: String, api_key: String, model: Option) -> Self { Self { endpoint, api_key, model, } } /// Check if the driver is configured (has endpoint and api_key) pub fn is_configured(&self) -> bool { !self.endpoint.is_empty() && !self.api_key.is_empty() } /// Call the LLM API with a simple prompt async fn call_llm(&self, prompt: String) -> Result { let client = reqwest::Client::new(); let model = self.model.clone().ok_or_else(|| { "Summary driver model not configured — kernel_init must be called first".to_string() })?; let request = serde_json::json!({ "model": model, "messages": [ { "role": "user", "content": prompt } ], "temperature": 0.3, "max_tokens": 200, }); let response = client .post(format!("{}/chat/completions", self.endpoint)) .header("Authorization", format!("Bearer {}", self.api_key)) .header("Content-Type", "application/json") .json(&request) .send() .await .map_err(|e| format!("Summary LLM request failed: {}", e))?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(format!("Summary LLM error {}: {}", status, body)); } let json: serde_json::Value = response .json() .await .map_err(|e| format!("Failed to parse summary response: {}", e))?; json.get("choices") .and_then(|c| c.get(0)) .and_then(|c| c.get("message")) .and_then(|m| m.get("content")) .and_then(|c| c.as_str()) .map(|s| s.to_string()) .ok_or_else(|| "Invalid summary LLM response format".to_string()) } } #[async_trait::async_trait] impl SummaryLlmDriver for TauriSummaryDriver { async fn generate_overview(&self, entry: &MemoryEntry) -> Result { let prompt = overview_prompt(entry); self.call_llm(prompt).await } async fn generate_abstract(&self, entry: &MemoryEntry) -> Result { let prompt = abstract_prompt(entry); self.call_llm(prompt).await } } /// Global summary driver instance (lazy-initialized) static SUMMARY_DRIVER: tokio::sync::OnceCell> = tokio::sync::OnceCell::const_new(); /// Configure the global summary driver pub fn configure_summary_driver(driver: TauriSummaryDriver) { let _ = SUMMARY_DRIVER.set(std::sync::Arc::new(driver)); tracing::info!("[SummarizerAdapter] Summary driver configured"); } /// Check if summary driver is available pub fn is_summary_driver_configured() -> bool { SUMMARY_DRIVER .get() .map(|d| d.is_configured()) .unwrap_or(false) } /// Get the global summary driver pub fn get_summary_driver() -> Option> { SUMMARY_DRIVER.get().cloned() } #[cfg(test)] mod tests { use super::*; use zclaw_growth::MemoryType; #[test] fn test_summary_driver_not_configured_by_default() { assert!(!is_summary_driver_configured()); } #[test] fn test_summary_driver_configure_and_check() { let driver = TauriSummaryDriver::new( "https://example.com/v1".to_string(), "test-key".to_string(), None, ); assert!(driver.is_configured()); let empty_driver = TauriSummaryDriver::new(String::new(), String::new(), None); assert!(!empty_driver.is_configured()); } }