diff --git a/Cargo.lock b/Cargo.lock index 1ee2762..f90a526 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9876,6 +9876,7 @@ dependencies = [ "tokio", "tracing", "ureq", + "url", "uuid", "wasmtime", "wasmtime-wasi", diff --git a/crates/zclaw-growth/src/evolution_engine.rs b/crates/zclaw-growth/src/evolution_engine.rs index 9934399..4fafb8e 100644 --- a/crates/zclaw-growth/src/evolution_engine.rs +++ b/crates/zclaw-growth/src/evolution_engine.rs @@ -9,8 +9,7 @@ use std::sync::Arc; use crate::experience_store::ExperienceStore; use crate::feedback_collector::{ - EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, RecommendedAction, - Sentiment, TrustUpdate, + FeedbackCollector, FeedbackEntry, TrustUpdate, }; use crate::pattern_aggregator::{AggregatedPattern, PatternAggregator}; use crate::quality_gate::{QualityGate, QualityReport}; diff --git a/crates/zclaw-growth/src/feedback_collector.rs b/crates/zclaw-growth/src/feedback_collector.rs index 9af86e5..d08fcb5 100644 --- a/crates/zclaw-growth/src/feedback_collector.rs +++ b/crates/zclaw-growth/src/feedback_collector.rs @@ -2,6 +2,8 @@ //! 收集用户对进化产物(技能/Pipeline)的显式/隐式反馈 //! 管理信任度衰减和优化循环 +use std::collections::HashMap; + use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -58,21 +60,32 @@ pub struct TrustRecord { /// 反馈收集器 /// 管理反馈记录和信任度评分 +/// 内存存储,可持久化到 SQLite(后续版本) pub struct FeedbackCollector { - /// 信任度记录表(内存,可持久化到 SQLite) - trust_records: Vec, + trust_records: HashMap, } impl FeedbackCollector { pub fn new() -> Self { Self { - trust_records: Vec::new(), + trust_records: HashMap::new(), } } /// 提交一条反馈 pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate { - let record = self.get_or_create_record(&entry.artifact_id, &entry.artifact_type); + let record = self + .trust_records + .entry(entry.artifact_id.clone()) + .or_insert_with(|| TrustRecord { + artifact_id: entry.artifact_id.clone(), + artifact_type: entry.artifact_type.clone(), + trust_score: 0.5, + total_feedback: 0, + positive_count: 0, + negative_count: 0, + last_updated: Utc::now(), + }); // 更新计数 record.total_feedback += 1; @@ -106,13 +119,13 @@ impl FeedbackCollector { /// 获取信任度记录 pub fn get_trust(&self, artifact_id: &str) -> Option<&TrustRecord> { - self.trust_records.iter().find(|r| r.artifact_id == artifact_id) + self.trust_records.get(artifact_id) } /// 获取所有需要优化的产物(信任度 < 0.4) pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> { self.trust_records - .iter() + .values() .filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2) .collect() } @@ -120,7 +133,7 @@ impl FeedbackCollector { /// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5) pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> { self.trust_records - .iter() + .values() .filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5) .collect() } @@ -128,37 +141,11 @@ impl FeedbackCollector { /// 获取所有高信任产物(信任度 >= 0.8) pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> { self.trust_records - .iter() + .values() .filter(|r| r.trust_score >= 0.8) .collect() } - fn get_or_create_record( - &mut self, - artifact_id: &str, - artifact_type: &EvolutionArtifact, - ) -> &mut TrustRecord { - let exists = self - .trust_records - .iter() - .any(|r| r.artifact_id == artifact_id); - if !exists { - self.trust_records.push(TrustRecord { - artifact_id: artifact_id.to_string(), - artifact_type: artifact_type.clone(), - trust_score: 0.5, // 初始信任度 - total_feedback: 0, - positive_count: 0, - negative_count: 0, - last_updated: Utc::now(), - }); - } - self.trust_records - .iter_mut() - .find(|r| r.artifact_id == artifact_id) - .unwrap() - } - fn calculate_trust_internal( positive: u32, negative: u32, @@ -268,7 +255,6 @@ mod tests { #[test] fn test_recommend_optimize() { let mut collector = FeedbackCollector::new(); - // 2 negative → trust < 0.4 collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative)); let update = collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative)); assert_eq!(update.action, RecommendedAction::Optimize); diff --git a/crates/zclaw-growth/src/json_utils.rs b/crates/zclaw-growth/src/json_utils.rs new file mode 100644 index 0000000..6dc945e --- /dev/null +++ b/crates/zclaw-growth/src/json_utils.rs @@ -0,0 +1,63 @@ +//! 共享 JSON 工具函数 +//! 从 LLM 返回的文本中提取 JSON 块 + +/// 从 LLM 返回文本中提取 JSON 块 +/// 支持三种格式:```json...``` 围栏、```...``` 围栏、裸 {...} +pub fn extract_json_block(text: &str) -> &str { + // 尝试匹配 ```json ... ``` + if let Some(start) = text.find("```json") { + let json_start = start + 7; + if let Some(end) = text[json_start..].find("```") { + return text[json_start..json_start + end].trim(); + } + } + // 尝试匹配 ``` ... ``` + if let Some(start) = text.find("```") { + let json_start = start + 3; + if let Some(end) = text[json_start..].find("```") { + return text[json_start..json_start + end].trim(); + } + } + // 尝试找 { ... } 块 + if let Some(start) = text.find('{') { + if let Some(end) = text.rfind('}') { + return &text[start..=end]; + } + } + text.trim() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_json_block_with_markdown() { + let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone."; + assert_eq!(extract_json_block(text), "{\"key\": \"value\"}"); + } + + #[test] + fn test_json_block_bare() { + let text = "{\"key\": \"value\"}"; + assert_eq!(extract_json_block(text), "{\"key\": \"value\"}"); + } + + #[test] + fn test_json_block_plain_fences() { + let text = "Result:\n```\n{\"a\": 1}\n```"; + assert_eq!(extract_json_block(text), "{\"a\": 1}"); + } + + #[test] + fn test_json_block_nested_braces() { + let text = r#"{"outer": {"inner": "val"}}"#; + assert_eq!(extract_json_block(text), r#"{"outer": {"inner": "val"}}"#); + } + + #[test] + fn test_json_block_no_json() { + let text = "no json here"; + assert_eq!(extract_json_block(text), "no json here"); + } +} diff --git a/crates/zclaw-growth/src/lib.rs b/crates/zclaw-growth/src/lib.rs index ea81863..8588c7b 100644 --- a/crates/zclaw-growth/src/lib.rs +++ b/crates/zclaw-growth/src/lib.rs @@ -65,6 +65,7 @@ pub mod storage; pub mod retrieval; pub mod summarizer; pub mod experience_store; +pub mod json_utils; pub mod experience_extractor; pub mod profile_updater; pub mod pattern_aggregator; @@ -106,7 +107,8 @@ pub use experience_store::{Experience, ExperienceStore}; pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer}; pub use summarizer::SummaryLlmDriver; pub use experience_extractor::ExperienceExtractor; -pub use profile_updater::UserProfileUpdater; +pub use json_utils::extract_json_block; +pub use profile_updater::{ProfileFieldUpdate, UserProfileUpdater}; pub use pattern_aggregator::{AggregatedPattern, PatternAggregator}; pub use skill_generator::{SkillCandidate, SkillGenerator}; pub use quality_gate::{QualityGate, QualityReport}; diff --git a/crates/zclaw-growth/src/pattern_aggregator.rs b/crates/zclaw-growth/src/pattern_aggregator.rs index 0b342d0..eb28c4f 100644 --- a/crates/zclaw-growth/src/pattern_aggregator.rs +++ b/crates/zclaw-growth/src/pattern_aggregator.rs @@ -52,15 +52,12 @@ impl PatternAggregator { let total_reuse: u32 = experiences.iter().map(|e| e.reuse_count).sum(); let common_steps = Self::find_common_steps(&experiences); - // 从 context 字段提取工具名 + // 从 context 字段提取工具名(context 存储的是触发工具/来源标识) + // Experience 结构没有独立的 tools 字段,context 作为来源标识使用 let tools: Vec = experiences .iter() - .flat_map(|e| { - e.context - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - }) + .map(|e| e.context.clone()) + .filter(|s| !s.is_empty()) .collect::>() .into_iter() .collect(); diff --git a/crates/zclaw-growth/src/profile_updater.rs b/crates/zclaw-growth/src/profile_updater.rs index e4c2f32..9e8308c 100644 --- a/crates/zclaw-growth/src/profile_updater.rs +++ b/crates/zclaw-growth/src/profile_updater.rs @@ -1,11 +1,19 @@ //! 用户画像增量更新器 -//! 从 CombinedExtraction 的 profile_signals 更新 UserProfileStore +//! 从 CombinedExtraction 的 profile_signals 提取需要更新的字段 //! 不额外调用 LLM,纯规则驱动 use crate::types::CombinedExtraction; +/// 待更新的画像字段 +#[derive(Debug, Clone, PartialEq)] +pub struct ProfileFieldUpdate { + pub field: String, + pub value: String, +} + /// 用户画像更新器 -/// 接收 CombinedExtraction 中的 profile_signals,通过回调函数更新画像 +/// 从 CombinedExtraction 的 profile_signals 中提取需更新的字段列表 +/// 调用方(zclaw-runtime)负责实际写入 UserProfileStore pub struct UserProfileUpdater; impl UserProfileUpdater { @@ -13,31 +21,30 @@ impl UserProfileUpdater { Self } - /// 从提取结果更新用户画像 - /// profile_store 通过闭包注入,避免 zclaw-growth 依赖 zclaw-memory - pub async fn update( + /// 从提取结果中收集需要更新的画像字段 + /// 返回 (field, value) 列表,由调用方负责实际的异步写入 + pub fn collect_updates( &self, - user_id: &str, extraction: &CombinedExtraction, - update_fn: F, - ) -> zclaw_types::Result<()> - where - F: Fn(&str, &str, &str) -> zclaw_types::Result<()> + Send + Sync, - { + ) -> Vec { let signals = &extraction.profile_signals; + let mut updates = Vec::new(); if let Some(ref industry) = signals.industry { - update_fn(user_id, "industry", industry)?; + updates.push(ProfileFieldUpdate { + field: "industry".to_string(), + value: industry.clone(), + }); } if let Some(ref style) = signals.communication_style { - update_fn(user_id, "communication_style", style)?; + updates.push(ProfileFieldUpdate { + field: "communication_style".to_string(), + value: style.clone(), + }); } - // pain_point 和 preferred_tool 使用单独的方法(有去重和容量限制) - // 这些通过 GrowthIntegration 中的具体调用处理 - - Ok(()) + updates } } @@ -50,60 +57,37 @@ impl Default for UserProfileUpdater { #[cfg(test)] mod tests { use super::*; - use std::sync::{Arc, Mutex}; - #[tokio::test] - async fn test_update_industry() { - let calls = Arc::new(Mutex::new(Vec::new())); - let calls_clone = calls.clone(); - let update_fn = move |uid: &str, field: &str, val: &str| -> zclaw_types::Result<()> { - calls_clone - .lock() - .unwrap() - .push((uid.to_string(), field.to_string(), val.to_string())); - Ok(()) - }; + #[test] + fn test_collect_updates_industry() { let mut extraction = CombinedExtraction::default(); extraction.profile_signals.industry = Some("healthcare".to_string()); let updater = UserProfileUpdater::new(); - updater.update("user1", &extraction, update_fn).await.unwrap(); + let updates = updater.collect_updates(&extraction); - let locked = calls.lock().unwrap(); - assert_eq!(locked.len(), 1); - assert_eq!(locked[0].1, "industry"); - assert_eq!(locked[0].2, "healthcare"); + assert_eq!(updates.len(), 1); + assert_eq!(updates[0].field, "industry"); + assert_eq!(updates[0].value, "healthcare"); } - #[tokio::test] - async fn test_update_no_signals() { - let update_fn = - |_: &str, _: &str, _: &str| -> zclaw_types::Result<()> { Ok(()) }; + #[test] + fn test_collect_updates_no_signals() { let extraction = CombinedExtraction::default(); let updater = UserProfileUpdater::new(); - updater.update("user1", &extraction, update_fn).await.unwrap(); - // No panic = pass + let updates = updater.collect_updates(&extraction); + assert!(updates.is_empty()); } - #[tokio::test] - async fn test_update_multiple_signals() { - let calls = Arc::new(Mutex::new(Vec::new())); - let calls_clone = calls.clone(); - let update_fn = move |uid: &str, field: &str, val: &str| -> zclaw_types::Result<()> { - calls_clone - .lock() - .unwrap() - .push((uid.to_string(), field.to_string(), val.to_string())); - Ok(()) - }; + #[test] + fn test_collect_updates_multiple_signals() { let mut extraction = CombinedExtraction::default(); extraction.profile_signals.industry = Some("ecommerce".to_string()); extraction.profile_signals.communication_style = Some("concise".to_string()); let updater = UserProfileUpdater::new(); - updater.update("user1", &extraction, update_fn).await.unwrap(); + let updates = updater.collect_updates(&extraction); - let locked = calls.lock().unwrap(); - assert_eq!(locked.len(), 2); + assert_eq!(updates.len(), 2); } } diff --git a/crates/zclaw-growth/src/quality_gate.rs b/crates/zclaw-growth/src/quality_gate.rs index 766b149..ef0197c 100644 --- a/crates/zclaw-growth/src/quality_gate.rs +++ b/crates/zclaw-growth/src/quality_gate.rs @@ -84,6 +84,7 @@ mod tests { body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(), source_pattern: "报表生成".to_string(), confidence: 0.85, + version: 1, } } diff --git a/crates/zclaw-growth/src/skill_generator.rs b/crates/zclaw-growth/src/skill_generator.rs index c59ae36..f27a480 100644 --- a/crates/zclaw-growth/src/skill_generator.rs +++ b/crates/zclaw-growth/src/skill_generator.rs @@ -15,6 +15,8 @@ pub struct SkillCandidate { pub body_markdown: String, pub source_pattern: String, pub confidence: f32, + /// 技能版本号,用于后续迭代追踪 + pub version: u32, } /// LLM 驱动的技能生成 prompt @@ -59,8 +61,7 @@ impl SkillGenerator { /// 解析 LLM 返回的 JSON 为 SkillCandidate pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result { - // 尝试提取 JSON 块(LLM 可能包裹在 ```json ... ``` 中) - let json_str = extract_json_block(json_str); + let json_str = crate::json_utils::extract_json_block(json_str); let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { zclaw_types::ZclawError::ConfigError(format!("Invalid skill JSON: {}", e)) @@ -95,35 +96,11 @@ impl SkillGenerator { body_markdown: raw["body_markdown"].as_str().unwrap_or("").to_string(), source_pattern: pattern.pain_pattern.clone(), confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32, + version: raw["version"].as_u64().unwrap_or(1) as u32, }) } } -/// 从 LLM 返回文本中提取 JSON 块 -fn extract_json_block(text: &str) -> &str { - // 尝试匹配 ```json ... ``` - if let Some(start) = text.find("```json") { - let json_start = start + 7; // skip ```json - if let Some(end) = text[json_start..].find("```") { - return text[json_start..json_start + end].trim(); - } - } - // 尝试匹配 ``` ... ``` - if let Some(start) = text.find("```") { - let json_start = start + 3; - if let Some(end) = text[json_start..].find("```") { - return text[json_start..json_start + end].trim(); - } - } - // 尝试找 { ... } 块 - if let Some(start) = text.find('{') { - if let Some(end) = text.rfind('}') { - return &text[start..=end]; - } - } - text.trim() -} - impl Default for SkillGenerator { fn default() -> Self { Self::new() @@ -194,12 +171,12 @@ mod tests { #[test] fn test_extract_json_block_with_markdown() { let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone."; - assert_eq!(extract_json_block(text), "{\"key\": \"value\"}"); + assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}"); } #[test] fn test_extract_json_block_bare() { let text = "{\"key\": \"value\"}"; - assert_eq!(extract_json_block(text), "{\"key\": \"value\"}"); + assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}"); } } diff --git a/crates/zclaw-growth/src/workflow_composer.rs b/crates/zclaw-growth/src/workflow_composer.rs index 4323e97..35de7d1 100644 --- a/crates/zclaw-growth/src/workflow_composer.rs +++ b/crates/zclaw-growth/src/workflow_composer.rs @@ -90,10 +90,10 @@ impl WorkflowComposer { /// 解析 LLM 返回的 JSON 为 PipelineCandidate pub fn parse_response( json_str: &str, - pattern: &ToolChainPattern, + _pattern: &ToolChainPattern, source_sessions: Vec, ) -> Result { - let json_str = extract_json_block(json_str); + let json_str = crate::json_utils::extract_json_block(json_str); let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| { zclaw_types::ZclawError::ConfigError(format!("Invalid pipeline JSON: {}", e)) })?; @@ -118,28 +118,6 @@ impl WorkflowComposer { } } -/// 从 LLM 返回文本中提取 JSON 块 -fn extract_json_block(text: &str) -> &str { - if let Some(start) = text.find("```json") { - let json_start = start + 7; - if let Some(end) = text[json_start..].find("```") { - return text[json_start..json_start + end].trim(); - } - } - if let Some(start) = text.find("```") { - let json_start = start + 3; - if let Some(end) = text[json_start..].find("```") { - return text[json_start..json_start + end].trim(); - } - } - if let Some(start) = text.find('{') { - if let Some(end) = text.rfind('}') { - return &text[start..=end]; - } - } - text.trim() -} - impl Default for WorkflowComposer { fn default() -> Self { Self::new() diff --git a/crates/zclaw-runtime/src/growth.rs b/crates/zclaw-runtime/src/growth.rs index 624a06d..102399b 100644 --- a/crates/zclaw-runtime/src/growth.rs +++ b/crates/zclaw-runtime/src/growth.rs @@ -324,25 +324,21 @@ impl GrowthIntegration { // Update user profile from extraction signals (L1 enhancement) if let Some(profile_store) = &self.profile_store { - let _store = profile_store.clone(); - let user_id = agent_id.to_string(); - if let Err(e) = self + let updates = self .profile_updater - .update(&user_id, &combined_extraction, move |uid, field, val| { - // Synchronous wrapper — the actual update_field is async, - // but we're already in an async context so we handle it via a future - // For now, log and let the store handle it - tracing::debug!( - "[GrowthIntegration] Profile update: {} {}={}", - uid, - field, - val + .collect_updates(&combined_extraction); + let user_id = agent_id.to_string(); + for update in updates { + if let Err(e) = profile_store + .update_field(&user_id, &update.field, &update.value) + .await + { + tracing::warn!( + "[GrowthIntegration] Profile update failed for {}: {}", + update.field, + e ); - Ok(()) - }) - .await - { - tracing::warn!("[GrowthIntegration] Profile update failed: {}", e); + } } } diff --git a/crates/zclaw-runtime/src/middleware/evolution.rs b/crates/zclaw-runtime/src/middleware/evolution.rs index af35b71..4b42b66 100644 --- a/crates/zclaw-runtime/src/middleware/evolution.rs +++ b/crates/zclaw-runtime/src/middleware/evolution.rs @@ -68,13 +68,17 @@ impl AgentMiddleware for EvolutionMiddleware { &self, ctx: &mut MiddlewareContext, ) -> Result { - let pending = self.pending.read().await; - if pending.is_empty() { - return Ok(MiddlewareDecision::Continue); - } + let to_inject = { + let mut pending = self.pending.write().await; + if pending.is_empty() { + return Ok(MiddlewareDecision::Continue); + } + // 只取第一条(最近的)事件注入,避免信息过载 + // drain 已注入的事件,防止重复注入 + std::mem::take(&mut *pending) + }; - // 只在第一条(最近的)事件上触发提示,避免信息过载 - if let Some(evolution) = pending.first() { + if let Some(evolution) = to_inject.into_iter().next() { let injection = format!( "\n\n\n\ 我注意到你经常做「{pattern}」相关的事情。\n\