/// Learn Handler Helpers /// /// Extracted to reduce learn_handler complexity. use actix_web::HttpResponse; use serde_json::{json, Value}; // ============================================================================ // Learn Parameters // ============================================================================ /// Validated learn request parameters #[derive(Debug, Clone)] pub struct LearnParams { pub project: String, pub text: String, pub question: String, pub memory_budget: u32, pub chunk_size: usize, pub model: String, } impl LearnParams { /// Parse and validate learn request body pub fn from_body(body: &Value) -> Result { let project = body["project"] .as_str() .unwrap_or("knowledge") .to_string(); let text = body["text"] .as_str() .filter(|t| !t.trim().is_empty()) .ok_or(LearnParamsError::MissingText)? .to_string(); let question = body["query"] .as_str() .unwrap_or("What are the key facts, patterns, and practices in this knowledge?") .to_string(); let memory_budget = body["memory_budget"] .as_u64() .unwrap_or(4096) .clamp(256, 32768) as u32; let chunk_size = body["chunk_size"] .as_u64() .unwrap_or(2000) .clamp(500, 10000) as usize; let model = body["model"] .as_str() .unwrap_or("qwen2.5:3b-instruct") .to_string(); Ok(Self { project, text, question, memory_budget, chunk_size, model }) } } /// Learn parameter validation errors #[derive(Debug, Clone, PartialEq, Eq)] pub enum LearnParamsError { MissingText, } impl LearnParamsError { pub fn to_response(&self) -> HttpResponse { let reason = match self { Self::MissingText => "missing required field: text", }; HttpResponse::BadRequest().json(json!({ "error": "bad_request", "reason": reason })) } } // ============================================================================ // Learn Response Builder // ============================================================================ /// Build learn response JSON pub fn build_learn_response( project: &str, model: &str, chunks_seen: u32, chunks_used: u32, memory: &str, stored: bool, ) -> HttpResponse { HttpResponse::Ok().json(json!({ "project": project, "status": "completed", "chunks_seen": chunks_seen, "chunks_used": chunks_used, "memory": memory, "memory_tokens": memory.len() / 4, "stored": stored, "model": model, })) } // ============================================================================ // Tests // ============================================================================ #[cfg(test)] mod tests { use super::*; #[test] fn test_learn_params_valid() { let body = json!({ "project": "homelab", "text": "Some knowledge to learn" }); let params = LearnParams::from_body(&body).unwrap(); assert_eq!(params.project, "homelab"); assert_eq!(params.text, "Some knowledge to learn"); assert_eq!(params.memory_budget, 4096); assert_eq!(params.chunk_size, 2000); } #[test] fn test_learn_params_defaults() { let body = json!({"text": "content"}); let params = LearnParams::from_body(&body).unwrap(); assert_eq!(params.project, "knowledge"); assert_eq!(params.model, "qwen2.5:3b-instruct"); } #[test] fn test_learn_params_custom_values() { let body = json!({ "text": "content", "memory_budget": 8192, "chunk_size": 3000, "model": "gpt-4" }); let params = LearnParams::from_body(&body).unwrap(); assert_eq!(params.memory_budget, 8192); assert_eq!(params.chunk_size, 3000); assert_eq!(params.model, "gpt-4"); } #[test] fn test_learn_params_clamped_budget() { let body = json!({"text": "x", "memory_budget": 999999}); let params = LearnParams::from_body(&body).unwrap(); assert_eq!(params.memory_budget, 32768); let body = json!({"text": "x", "memory_budget": 10}); let params = LearnParams::from_body(&body).unwrap(); assert_eq!(params.memory_budget, 256); } #[test] fn test_learn_params_missing_text() { let body = json!({"project": "test"}); assert_eq!(LearnParams::from_body(&body).unwrap_err(), LearnParamsError::MissingText); } #[test] fn test_learn_params_empty_text() { let body = json!({"text": " "}); assert_eq!(LearnParams::from_body(&body).unwrap_err(), LearnParamsError::MissingText); } }