//! 结构化经验提取器 //! 从对话中提取 ExperienceCandidate(pain_pattern → solution_steps → outcome) //! 持久化到 ExperienceStore use std::sync::Arc; use crate::experience_store::ExperienceStore; use crate::types::{CombinedExtraction, Outcome}; /// 结构化经验提取器 /// LLM 调用已由上层 MemoryExtractor 完成,这里只做解析和持久化 pub struct ExperienceExtractor { store: Option>, } impl ExperienceExtractor { pub fn new() -> Self { Self { store: None } } pub fn with_store(mut self, store: Arc) -> Self { self.store = Some(store); self } /// 从 CombinedExtraction 中提取经验并持久化 /// LLM 调用已由上层完成,这里只做解析和存储 pub async fn persist_experiences( &self, agent_id: &str, extraction: &CombinedExtraction, ) -> zclaw_types::Result { let store = match &self.store { Some(s) => s, None => return Ok(0), }; let mut count = 0; for candidate in &extraction.experiences { if candidate.confidence < 0.6 { continue; } let outcome_str = match candidate.outcome { Outcome::Success => "success", Outcome::Partial => "partial", Outcome::Failed => "failed", }; let mut exp = crate::experience_store::Experience::new( agent_id, &candidate.pain_pattern, &candidate.context, candidate.solution_steps.clone(), outcome_str, ); // 填充 tool_used:取 tools_used 中的第一个作为主要工具 exp.tool_used = candidate.tools_used.first().cloned(); exp.industry_context = candidate.industry_context.clone(); store.store_experience(&exp).await?; count += 1; } Ok(count) } } impl Default for ExperienceExtractor { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; use crate::types::{ExperienceCandidate, Outcome}; #[test] fn test_extractor_new_without_store() { let ext = ExperienceExtractor::new(); assert!(ext.store.is_none()); } #[tokio::test] async fn test_persist_no_store_returns_zero() { let ext = ExperienceExtractor::new(); let extraction = CombinedExtraction::default(); let count = ext.persist_experiences("agent1", &extraction).await.unwrap(); assert_eq!(count, 0); } #[tokio::test] async fn test_persist_filters_low_confidence() { let viking = Arc::new(crate::VikingAdapter::in_memory()); let store = Arc::new(ExperienceStore::new(viking)); let ext = ExperienceExtractor::new().with_store(store); let mut extraction = CombinedExtraction::default(); extraction.experiences.push(ExperienceCandidate { pain_pattern: "low confidence task".to_string(), context: "should be filtered".to_string(), solution_steps: vec!["step1".to_string()], outcome: Outcome::Success, confidence: 0.3, // 低于 0.6 阈值 tools_used: vec![], industry_context: None, }); extraction.experiences.push(ExperienceCandidate { pain_pattern: "high confidence task".to_string(), context: "should be stored".to_string(), solution_steps: vec!["step1".to_string(), "step2".to_string()], outcome: Outcome::Success, confidence: 0.9, tools_used: vec!["researcher".to_string()], industry_context: Some("healthcare".to_string()), }); let count = ext.persist_experiences("agent-1", &extraction).await.unwrap(); assert_eq!(count, 1); // 只有 1 个通过置信度过滤 } }