From f97e6fdbb628175ee402eb7a6d10f019b2407a89 Mon Sep 17 00:00:00 2001 From: iven Date: Sat, 18 Apr 2026 21:27:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Evolution=20Engine=20Phase=203-5=20?= =?UTF-8?q?=E2=80=94=20WorkflowComposer+FeedbackCollector+EvolutionMiddlew?= =?UTF-8?q?are+=E5=8F=8D=E9=A6=88=E9=97=AD=E7=8E=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3: - EvolutionMiddleware (priority 78): 管家对话中注入进化确认提示 - GrowthIntegration.check_evolution() API 串入 Phase 4: - WorkflowComposer: 轨迹工具链模式聚类 + Pipeline YAML prompt 构建 + JSON 解析 - EvolutionEngine.analyze_trajectory_patterns() L3 入口 Phase 5: - FeedbackCollector: 反馈信号收集 + 信任度管理 + 推荐(Optimize/Archive/Promote) - EvolutionEngine 反馈闭环方法: submit_feedback/get_artifacts_needing_optimization 新增 12 个测试(111→123),全 workspace 701 测试通过。 --- crates/zclaw-growth/src/evolution_engine.rs | 80 ++++- crates/zclaw-growth/src/feedback_collector.rs | 298 ++++++++++++++++++ crates/zclaw-growth/src/lib.rs | 7 + crates/zclaw-growth/src/workflow_composer.rs | 211 +++++++++++++ crates/zclaw-runtime/src/middleware.rs | 1 + .../zclaw-runtime/src/middleware/evolution.rs | 134 ++++++++ 6 files changed, 727 insertions(+), 4 deletions(-) create mode 100644 crates/zclaw-growth/src/feedback_collector.rs create mode 100644 crates/zclaw-growth/src/workflow_composer.rs create mode 100644 crates/zclaw-runtime/src/middleware/evolution.rs diff --git a/crates/zclaw-growth/src/evolution_engine.rs b/crates/zclaw-growth/src/evolution_engine.rs index 9f810db..9934399 100644 --- a/crates/zclaw-growth/src/evolution_engine.rs +++ b/crates/zclaw-growth/src/evolution_engine.rs @@ -2,14 +2,20 @@ //! 协调 L1/L2/L3 三层进化的触发和执行 //! L1 (记忆进化) 在 GrowthIntegration 中处理 //! L2 (技能进化) 通过 PatternAggregator + SkillGenerator + QualityGate 协调 -//! L3 (工作流进化) 预留接口,Phase 4 实现 +//! L3 (工作流进化) 通过 WorkflowComposer 协调 +//! 反馈闭环通过 FeedbackCollector 管理 use std::sync::Arc; use crate::experience_store::ExperienceStore; +use crate::feedback_collector::{ + EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, RecommendedAction, + Sentiment, TrustUpdate, +}; use crate::pattern_aggregator::{AggregatedPattern, PatternAggregator}; use crate::quality_gate::{QualityGate, QualityReport}; use crate::skill_generator::{SkillCandidate, SkillGenerator}; +use crate::workflow_composer::{ToolChainPattern, WorkflowComposer}; use crate::VikingAdapter; use zclaw_types::Result; @@ -37,6 +43,7 @@ impl Default for EvolutionConfig { /// 进化引擎中枢 pub struct EvolutionEngine { viking: Arc, + feedback: FeedbackCollector, config: EvolutionConfig, } @@ -44,17 +51,16 @@ impl EvolutionEngine { pub fn new(viking: Arc) -> Self { Self { viking, + feedback: FeedbackCollector::new(), config: EvolutionConfig::default(), } } /// Backward-compatible constructor pub fn from_experience_store(_experience_store: Arc) -> Self { - // Extract viking from ExperienceStore — we need the underlying adapter - // Since ExperienceStore holds Arc, we create a new in-memory one - // For proper usage, use new() with the correct viking adapter Self { viking: Arc::new(VikingAdapter::in_memory()), + feedback: FeedbackCollector::new(), config: EvolutionConfig::default(), } } @@ -106,6 +112,72 @@ impl EvolutionEngine { pub fn config(&self) -> &EvolutionConfig { &self.config } + + // ----------------------------------------------------------------------- + // L3: 工作流进化 + // ----------------------------------------------------------------------- + + /// L3: 从轨迹数据中提取重复的工具链模式 + pub fn analyze_trajectory_patterns( + &self, + trajectories: &[(String, Vec)], // (session_id, tools_used) + ) -> Vec<(ToolChainPattern, Vec)> { + if !self.config.enabled { + return Vec::new(); + } + WorkflowComposer::extract_patterns(trajectories) + } + + /// L3: 为给定工具链模式构建工作流生成 prompt + pub fn build_workflow_prompt( + &self, + pattern: &ToolChainPattern, + frequency: usize, + industry: Option<&str>, + ) -> String { + WorkflowComposer::build_prompt(pattern, frequency, industry) + } + + // ----------------------------------------------------------------------- + // 反馈闭环 + // ----------------------------------------------------------------------- + + /// 提交反馈并获取信任度更新 + pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate { + self.feedback.submit_feedback(entry) + } + + /// 获取需要优化的进化产物 + pub fn get_artifacts_needing_optimization(&self) -> Vec { + self.feedback + .get_artifacts_needing_optimization() + .iter() + .map(|r| r.artifact_id.clone()) + .collect() + } + + /// 获取建议归档的进化产物 + pub fn get_artifacts_to_archive(&self) -> Vec { + self.feedback + .get_artifacts_to_archive() + .iter() + .map(|r| r.artifact_id.clone()) + .collect() + } + + /// 获取推荐产物 + pub fn get_recommended_artifacts(&self) -> Vec { + self.feedback + .get_recommended_artifacts() + .iter() + .map(|r| r.artifact_id.clone()) + .collect() + } + + /// 获取反馈收集器的引用(用于高级查询) + pub fn feedback(&self) -> &FeedbackCollector { + &self.feedback + } } #[cfg(test)] diff --git a/crates/zclaw-growth/src/feedback_collector.rs b/crates/zclaw-growth/src/feedback_collector.rs new file mode 100644 index 0000000..9af86e5 --- /dev/null +++ b/crates/zclaw-growth/src/feedback_collector.rs @@ -0,0 +1,298 @@ +//! 反馈信号收集与信任度管理(Phase 5 反馈闭环) +//! 收集用户对进化产物(技能/Pipeline)的显式/隐式反馈 +//! 管理信任度衰减和优化循环 + +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// 反馈信号类型 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum FeedbackSignal { + /// 用户直接表达的意见 + Explicit, + /// 从使用行为推断 + ImplicitUsage, + /// 使用频率 + UsageCount, + /// 任务完成率 + CompletionRate, +} + +/// 情感倾向 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum Sentiment { + Positive, + Negative, + Neutral, +} + +/// 进化产物类型 +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum EvolutionArtifact { + Skill, + Pipeline, +} + +/// 单条反馈记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FeedbackEntry { + pub artifact_id: String, + pub artifact_type: EvolutionArtifact, + pub signal: FeedbackSignal, + pub sentiment: Sentiment, + pub details: Option, + pub timestamp: DateTime, +} + +/// 信任度记录 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrustRecord { + pub artifact_id: String, + pub artifact_type: EvolutionArtifact, + pub trust_score: f32, + pub total_feedback: u32, + pub positive_count: u32, + pub negative_count: u32, + pub last_updated: DateTime, +} + +/// 反馈收集器 +/// 管理反馈记录和信任度评分 +pub struct FeedbackCollector { + /// 信任度记录表(内存,可持久化到 SQLite) + trust_records: Vec, +} + +impl FeedbackCollector { + pub fn new() -> Self { + Self { + trust_records: Vec::new(), + } + } + + /// 提交一条反馈 + pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate { + let record = self.get_or_create_record(&entry.artifact_id, &entry.artifact_type); + + // 更新计数 + record.total_feedback += 1; + match entry.sentiment { + Sentiment::Positive => record.positive_count += 1, + Sentiment::Negative => record.negative_count += 1, + Sentiment::Neutral => {} + } + + // 重新计算信任度 + let old_score = record.trust_score; + record.trust_score = Self::calculate_trust_internal( + record.positive_count, + record.negative_count, + record.total_feedback, + record.last_updated, + ); + record.last_updated = Utc::now(); + + let new_score = record.trust_score; + let total = record.total_feedback; + let action = Self::recommend_action_internal(new_score, total); + + TrustUpdate { + artifact_id: entry.artifact_id.clone(), + old_score, + new_score, + action, + } + } + + /// 获取信任度记录 + pub fn get_trust(&self, artifact_id: &str) -> Option<&TrustRecord> { + self.trust_records.iter().find(|r| r.artifact_id == artifact_id) + } + + /// 获取所有需要优化的产物(信任度 < 0.4) + pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> { + self.trust_records + .iter() + .filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2) + .collect() + } + + /// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5) + pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> { + self.trust_records + .iter() + .filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5) + .collect() + } + + /// 获取所有高信任产物(信任度 >= 0.8) + pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> { + self.trust_records + .iter() + .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, + total: u32, + last_updated: DateTime, + ) -> f32 { + if total == 0 { + return 0.5; + } + let positive_ratio = positive as f32 / total as f32; + let negative_penalty = negative as f32 * 0.1; + let days_since = (Utc::now() - last_updated).num_days().max(0) as f32; + let time_decay = 1.0 - (days_since * 0.005).min(0.5); + (positive_ratio * time_decay - negative_penalty).clamp(0.0, 1.0) + } + + fn recommend_action_internal(trust_score: f32, total_feedback: u32) -> RecommendedAction { + if trust_score >= 0.8 { + RecommendedAction::Promote + } else if trust_score < 0.2 && total_feedback >= 5 { + RecommendedAction::Archive + } else if trust_score < 0.4 && total_feedback >= 2 { + RecommendedAction::Optimize + } else { + RecommendedAction::Monitor + } + } +} + +impl Default for FeedbackCollector { + fn default() -> Self { + Self::new() + } +} + +/// 信任度更新结果 +#[derive(Debug, Clone)] +pub struct TrustUpdate { + pub artifact_id: String, + pub old_score: f32, + pub new_score: f32, + pub action: RecommendedAction, +} + +/// 建议动作 +#[derive(Debug, Clone, PartialEq)] +pub enum RecommendedAction { + /// 继续观察 + Monitor, + /// 需要优化 + Optimize, + /// 建议归档(降级为记忆) + Archive, + /// 建议提升为推荐技能 + Promote, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_feedback(artifact_id: &str, sentiment: Sentiment) -> FeedbackEntry { + FeedbackEntry { + artifact_id: artifact_id.to_string(), + artifact_type: EvolutionArtifact::Skill, + signal: FeedbackSignal::Explicit, + sentiment, + details: None, + timestamp: Utc::now(), + } + } + + #[test] + fn test_initial_trust() { + let collector = FeedbackCollector::new(); + assert!(collector.get_trust("skill-1").is_none()); + } + + #[test] + fn test_positive_feedback_increases_trust() { + let mut collector = FeedbackCollector::new(); + collector.submit_feedback(make_feedback("skill-1", Sentiment::Positive)); + let record = collector.get_trust("skill-1").unwrap(); + assert!(record.trust_score > 0.5); + assert_eq!(record.positive_count, 1); + } + + #[test] + fn test_negative_feedback_decreases_trust() { + let mut collector = FeedbackCollector::new(); + collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative)); + let record = collector.get_trust("skill-1").unwrap(); + assert!(record.trust_score < 0.5); + } + + #[test] + fn test_mixed_feedback() { + let mut collector = FeedbackCollector::new(); + collector.submit_feedback(make_feedback("skill-1", Sentiment::Positive)); + collector.submit_feedback(make_feedback("skill-1", Sentiment::Positive)); + collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative)); + let record = collector.get_trust("skill-1").unwrap(); + assert_eq!(record.total_feedback, 3); + assert!(record.trust_score > 0.3); // 2/3 positive + } + + #[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); + } + + #[test] + fn test_needs_optimization_filter() { + let mut collector = FeedbackCollector::new(); + collector.submit_feedback(make_feedback("bad-skill", Sentiment::Negative)); + collector.submit_feedback(make_feedback("bad-skill", Sentiment::Negative)); + collector.submit_feedback(make_feedback("good-skill", Sentiment::Positive)); + + let needs = collector.get_artifacts_needing_optimization(); + assert_eq!(needs.len(), 1); + assert_eq!(needs[0].artifact_id, "bad-skill"); + } + + #[test] + fn test_promote_recommendation() { + let mut collector = FeedbackCollector::new(); + for _ in 0..5 { + collector.submit_feedback(make_feedback("great-skill", Sentiment::Positive)); + } + let recommended = collector.get_recommended_artifacts(); + assert_eq!(recommended.len(), 1); + } +} diff --git a/crates/zclaw-growth/src/lib.rs b/crates/zclaw-growth/src/lib.rs index 0c1c1c6..ea81863 100644 --- a/crates/zclaw-growth/src/lib.rs +++ b/crates/zclaw-growth/src/lib.rs @@ -71,6 +71,8 @@ pub mod pattern_aggregator; pub mod skill_generator; pub mod quality_gate; pub mod evolution_engine; +pub mod workflow_composer; +pub mod feedback_collector; // Re-export main types for convenience pub use types::{ @@ -109,6 +111,11 @@ pub use pattern_aggregator::{AggregatedPattern, PatternAggregator}; pub use skill_generator::{SkillCandidate, SkillGenerator}; pub use quality_gate::{QualityGate, QualityReport}; pub use evolution_engine::{EvolutionConfig, EvolutionEngine}; +pub use workflow_composer::{PipelineCandidate, ToolChainPattern, WorkflowComposer}; +pub use feedback_collector::{ + EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, + RecommendedAction, Sentiment, TrustRecord, TrustUpdate, +}; /// Growth system configuration #[derive(Debug, Clone)] diff --git a/crates/zclaw-growth/src/workflow_composer.rs b/crates/zclaw-growth/src/workflow_composer.rs new file mode 100644 index 0000000..4323e97 --- /dev/null +++ b/crates/zclaw-growth/src/workflow_composer.rs @@ -0,0 +1,211 @@ +//! 工作流组装器(L3 工作流进化) +//! 从轨迹数据中分析重复的工具链模式,自动组装 Pipeline YAML +//! 触发条件:CompressedTrajectory 中出现 2 次以上相同工具链序列 + +use zclaw_types::Result; + +/// Pipeline 候选项 +#[derive(Debug, Clone)] +pub struct PipelineCandidate { + pub name: String, + pub description: String, + pub triggers: Vec, + pub yaml_content: String, + pub source_sessions: Vec, + pub confidence: f32, +} + +/// 工具链模式(用于聚类分析) +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub struct ToolChainPattern { + pub steps: Vec, +} + +/// 工作流组装 prompt +const WORKFLOW_GENERATION_PROMPT: &str = r#" +你是一个工作流设计专家。根据以下用户反复执行的工具链序列,设计一个可复用的 Pipeline 工作流。 + +工具链序列:{tool_chain} +执行频率:{frequency} 次 +行业背景:{industry} + +请生成以下 JSON: +```json +{ + "name": "工作流名称(简短中文)", + "description": "工作流描述", + "triggers": ["触发词1", "触发词2"], + "yaml_content": "Pipeline YAML 内容", + "confidence": 0.8 +} +``` +"#; + +/// 工作流组装器 +/// 分析压缩轨迹中的工具链模式,通过 LLM 生成 Pipeline YAML +pub struct WorkflowComposer; + +impl WorkflowComposer { + pub fn new() -> Self { + Self + } + + /// 从压缩轨迹的工具链中提取模式 + /// 简单的精确匹配聚类:相同工具链序列视为同一模式 + pub fn extract_patterns( + trajectories: &[(String, Vec)], // (session_id, tools_used) + ) -> Vec<(ToolChainPattern, Vec)> { + use std::collections::HashMap; + + let mut groups: HashMap> = HashMap::new(); + for (session_id, tools) in trajectories { + if tools.len() < 2 { + continue; // 单步操作不构成工作流 + } + let pattern = ToolChainPattern { + steps: tools.clone(), + }; + groups.entry(pattern).or_default().push(session_id.clone()); + } + + // 过滤出现 2 次以上的模式 + groups + .into_iter() + .filter(|(_, sessions)| sessions.len() >= 2) + .collect() + } + + /// 构建 LLM prompt + pub fn build_prompt( + pattern: &ToolChainPattern, + frequency: usize, + industry: Option<&str>, + ) -> String { + WORKFLOW_GENERATION_PROMPT + .replace("{tool_chain}", &pattern.steps.join(" → ")) + .replace("{frequency}", &frequency.to_string()) + .replace("{industry}", industry.unwrap_or("通用")) + } + + /// 解析 LLM 返回的 JSON 为 PipelineCandidate + pub fn parse_response( + json_str: &str, + pattern: &ToolChainPattern, + source_sessions: Vec, + ) -> Result { + let json_str = 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)) + })?; + + let triggers: Vec = raw["triggers"] + .as_array() + .map(|a: &Vec| { + a.iter() + .filter_map(|v: &serde_json::Value| v.as_str().map(String::from)) + .collect() + }) + .unwrap_or_default(); + + Ok(PipelineCandidate { + name: raw["name"].as_str().unwrap_or("未命名工作流").to_string(), + description: raw["description"].as_str().unwrap_or("").to_string(), + triggers, + yaml_content: raw["yaml_content"].as_str().unwrap_or("").to_string(), + source_sessions, + confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32, + }) + } +} + +/// 从 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() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_patterns_filters_single_step() { + let trajectories = vec![ + ("s1".to_string(), vec!["researcher".to_string()]), + ]; + let patterns = WorkflowComposer::extract_patterns(&trajectories); + assert!(patterns.is_empty()); + } + + #[test] + fn test_extract_patterns_groups_identical_chains() { + let trajectories = vec![ + ("s1".to_string(), vec!["researcher".into(), "collector".into()]), + ("s2".to_string(), vec!["researcher".into(), "collector".into()]), + ("s3".to_string(), vec!["browser".into()]), // 单步,过滤 + ]; + let patterns = WorkflowComposer::extract_patterns(&trajectories); + assert_eq!(patterns.len(), 1); + assert_eq!(patterns[0].1.len(), 2); // 2 sessions + } + + #[test] + fn test_extract_patterns_requires_min_2() { + let trajectories = vec![ + ("s1".to_string(), vec!["a".into(), "b".into()]), + ]; + let patterns = WorkflowComposer::extract_patterns(&trajectories); + assert!(patterns.is_empty()); // 只出现 1 次 + } + + #[test] + fn test_build_prompt() { + let pattern = ToolChainPattern { + steps: vec!["researcher".into(), "collector".into(), "summarize".into()], + }; + let prompt = WorkflowComposer::build_prompt(&pattern, 3, Some("healthcare")); + assert!(prompt.contains("researcher")); + assert!(prompt.contains("3")); + assert!(prompt.contains("healthcare")); + } + + #[test] + fn test_parse_response() { + let pattern = ToolChainPattern { + steps: vec!["researcher".into()], + }; + let json = r##"{"name":"每日简报","description":"搜索+汇总","triggers":["简报","日报"],"yaml_content":"steps: []","confidence":0.85}"##; + let candidate = WorkflowComposer::parse_response( + json, + &pattern, + vec!["s1".into(), "s2".into()], + ) + .unwrap(); + assert_eq!(candidate.name, "每日简报"); + assert_eq!(candidate.triggers.len(), 2); + assert_eq!(candidate.source_sessions.len(), 2); + assert!((candidate.confidence - 0.85).abs() < 0.01); + } +} diff --git a/crates/zclaw-runtime/src/middleware.rs b/crates/zclaw-runtime/src/middleware.rs index 539007f..383292e 100644 --- a/crates/zclaw-runtime/src/middleware.rs +++ b/crates/zclaw-runtime/src/middleware.rs @@ -279,3 +279,4 @@ pub mod token_calibration; pub mod tool_error; pub mod tool_output_guard; pub mod trajectory_recorder; +pub mod evolution; diff --git a/crates/zclaw-runtime/src/middleware/evolution.rs b/crates/zclaw-runtime/src/middleware/evolution.rs new file mode 100644 index 0000000..af35b71 --- /dev/null +++ b/crates/zclaw-runtime/src/middleware/evolution.rs @@ -0,0 +1,134 @@ +//! 进化引擎中间件 +//! 在管家对话中检测并呈现"技能进化确认"提示 +//! 优先级 78(在 ButlerRouter@80 之前运行) + +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::RwLock; +use crate::middleware::{ + AgentMiddleware, MiddlewareContext, MiddlewareDecision, +}; +use zclaw_types::Result; + +/// 待确认的进化事件 +#[derive(Debug, Clone)] +pub struct PendingEvolution { + pub pattern_name: String, + pub trigger_suggestion: String, + pub description: String, +} + +/// 进化引擎中间件 +/// 检查是否有待确认的进化事件,注入确认提示到 system prompt +pub struct EvolutionMiddleware { + pending: Arc>>, +} + +impl EvolutionMiddleware { + pub fn new() -> Self { + Self { + pending: Arc::new(RwLock::new(Vec::new())), + } + } + + /// 添加一个待确认的进化事件 + pub async fn add_pending(&self, evolution: PendingEvolution) { + self.pending.write().await.push(evolution); + } + + /// 获取并清除所有待确认事件 + pub async fn drain_pending(&self) -> Vec { + let mut pending = self.pending.write().await; + std::mem::take(&mut *pending) + } + + /// 当前待确认事件数量 + pub async fn pending_count(&self) -> usize { + self.pending.read().await.len() + } +} + +impl Default for EvolutionMiddleware { + fn default() -> Self { + Self::new() + } +} + +#[async_trait] +impl AgentMiddleware for EvolutionMiddleware { + fn name(&self) -> &str { + "evolution" + } + + fn priority(&self) -> i32 { + 78 // 在 ButlerRouter(80) 之前 + } + + async fn before_completion( + &self, + ctx: &mut MiddlewareContext, + ) -> Result { + let pending = self.pending.read().await; + if pending.is_empty() { + return Ok(MiddlewareDecision::Continue); + } + + // 只在第一条(最近的)事件上触发提示,避免信息过载 + if let Some(evolution) = pending.first() { + let injection = format!( + "\n\n\n\ + 我注意到你经常做「{pattern}」相关的事情。\n\ + 我可以帮你整理成一个技能,以后直接说「{trigger}」就能用了。\n\ + 技能描述:{desc}\n\ + 如果你同意,请回复 '确认保存技能'。如果你想调整,可以告诉我怎么改。\n\ + ", + pattern = evolution.pattern_name, + trigger = evolution.trigger_suggestion, + desc = evolution.description, + ); + ctx.system_prompt.push_str(&injection); + + tracing::info!( + "[EvolutionMiddleware] Injected evolution suggestion for: {}", + evolution.pattern_name + ); + } + + Ok(MiddlewareDecision::Continue) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_no_pending_continues() { + let mw = EvolutionMiddleware::new(); + assert_eq!(mw.pending_count().await, 0); + } + + #[tokio::test] + async fn test_add_and_drain() { + let mw = EvolutionMiddleware::new(); + mw.add_pending(PendingEvolution { + pattern_name: "报表生成".to_string(), + trigger_suggestion: "生成报表".to_string(), + description: "自动生成每日报表".to_string(), + }) + .await; + assert_eq!(mw.pending_count().await, 1); + + let drained = mw.drain_pending().await; + assert_eq!(drained.len(), 1); + assert_eq!(drained[0].pattern_name, "报表生成"); + assert_eq!(mw.pending_count().await, 0); + } + + #[tokio::test] + async fn test_name_and_priority() { + let mw = EvolutionMiddleware::new(); + assert_eq!(mw.name(), "evolution"); + assert_eq!(mw.priority(), 78); + } +}