feat: Evolution Engine Phase 3-5 — WorkflowComposer+FeedbackCollector+EvolutionMiddleware+反馈闭环
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
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 测试通过。
This commit is contained in:
@@ -2,14 +2,20 @@
|
|||||||
//! 协调 L1/L2/L3 三层进化的触发和执行
|
//! 协调 L1/L2/L3 三层进化的触发和执行
|
||||||
//! L1 (记忆进化) 在 GrowthIntegration 中处理
|
//! L1 (记忆进化) 在 GrowthIntegration 中处理
|
||||||
//! L2 (技能进化) 通过 PatternAggregator + SkillGenerator + QualityGate 协调
|
//! L2 (技能进化) 通过 PatternAggregator + SkillGenerator + QualityGate 协调
|
||||||
//! L3 (工作流进化) 预留接口,Phase 4 实现
|
//! L3 (工作流进化) 通过 WorkflowComposer 协调
|
||||||
|
//! 反馈闭环通过 FeedbackCollector 管理
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::experience_store::ExperienceStore;
|
use crate::experience_store::ExperienceStore;
|
||||||
|
use crate::feedback_collector::{
|
||||||
|
EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, RecommendedAction,
|
||||||
|
Sentiment, TrustUpdate,
|
||||||
|
};
|
||||||
use crate::pattern_aggregator::{AggregatedPattern, PatternAggregator};
|
use crate::pattern_aggregator::{AggregatedPattern, PatternAggregator};
|
||||||
use crate::quality_gate::{QualityGate, QualityReport};
|
use crate::quality_gate::{QualityGate, QualityReport};
|
||||||
use crate::skill_generator::{SkillCandidate, SkillGenerator};
|
use crate::skill_generator::{SkillCandidate, SkillGenerator};
|
||||||
|
use crate::workflow_composer::{ToolChainPattern, WorkflowComposer};
|
||||||
use crate::VikingAdapter;
|
use crate::VikingAdapter;
|
||||||
use zclaw_types::Result;
|
use zclaw_types::Result;
|
||||||
|
|
||||||
@@ -37,6 +43,7 @@ impl Default for EvolutionConfig {
|
|||||||
/// 进化引擎中枢
|
/// 进化引擎中枢
|
||||||
pub struct EvolutionEngine {
|
pub struct EvolutionEngine {
|
||||||
viking: Arc<VikingAdapter>,
|
viking: Arc<VikingAdapter>,
|
||||||
|
feedback: FeedbackCollector,
|
||||||
config: EvolutionConfig,
|
config: EvolutionConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,17 +51,16 @@ impl EvolutionEngine {
|
|||||||
pub fn new(viking: Arc<VikingAdapter>) -> Self {
|
pub fn new(viking: Arc<VikingAdapter>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
viking,
|
viking,
|
||||||
|
feedback: FeedbackCollector::new(),
|
||||||
config: EvolutionConfig::default(),
|
config: EvolutionConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backward-compatible constructor
|
/// Backward-compatible constructor
|
||||||
pub fn from_experience_store(_experience_store: Arc<ExperienceStore>) -> Self {
|
pub fn from_experience_store(_experience_store: Arc<ExperienceStore>) -> Self {
|
||||||
// Extract viking from ExperienceStore — we need the underlying adapter
|
|
||||||
// Since ExperienceStore holds Arc<VikingAdapter>, we create a new in-memory one
|
|
||||||
// For proper usage, use new() with the correct viking adapter
|
|
||||||
Self {
|
Self {
|
||||||
viking: Arc::new(VikingAdapter::in_memory()),
|
viking: Arc::new(VikingAdapter::in_memory()),
|
||||||
|
feedback: FeedbackCollector::new(),
|
||||||
config: EvolutionConfig::default(),
|
config: EvolutionConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,6 +112,72 @@ impl EvolutionEngine {
|
|||||||
pub fn config(&self) -> &EvolutionConfig {
|
pub fn config(&self) -> &EvolutionConfig {
|
||||||
&self.config
|
&self.config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// L3: 工作流进化
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// L3: 从轨迹数据中提取重复的工具链模式
|
||||||
|
pub fn analyze_trajectory_patterns(
|
||||||
|
&self,
|
||||||
|
trajectories: &[(String, Vec<String>)], // (session_id, tools_used)
|
||||||
|
) -> Vec<(ToolChainPattern, Vec<String>)> {
|
||||||
|
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<String> {
|
||||||
|
self.feedback
|
||||||
|
.get_artifacts_needing_optimization()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.artifact_id.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取建议归档的进化产物
|
||||||
|
pub fn get_artifacts_to_archive(&self) -> Vec<String> {
|
||||||
|
self.feedback
|
||||||
|
.get_artifacts_to_archive()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.artifact_id.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取推荐产物
|
||||||
|
pub fn get_recommended_artifacts(&self) -> Vec<String> {
|
||||||
|
self.feedback
|
||||||
|
.get_recommended_artifacts()
|
||||||
|
.iter()
|
||||||
|
.map(|r| r.artifact_id.clone())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取反馈收集器的引用(用于高级查询)
|
||||||
|
pub fn feedback(&self) -> &FeedbackCollector {
|
||||||
|
&self.feedback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
298
crates/zclaw-growth/src/feedback_collector.rs
Normal file
298
crates/zclaw-growth/src/feedback_collector.rs
Normal file
@@ -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<String>,
|
||||||
|
pub timestamp: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 信任度记录
|
||||||
|
#[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<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 反馈收集器
|
||||||
|
/// 管理反馈记录和信任度评分
|
||||||
|
pub struct FeedbackCollector {
|
||||||
|
/// 信任度记录表(内存,可持久化到 SQLite)
|
||||||
|
trust_records: Vec<TrustRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Utc>,
|
||||||
|
) -> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,6 +71,8 @@ pub mod pattern_aggregator;
|
|||||||
pub mod skill_generator;
|
pub mod skill_generator;
|
||||||
pub mod quality_gate;
|
pub mod quality_gate;
|
||||||
pub mod evolution_engine;
|
pub mod evolution_engine;
|
||||||
|
pub mod workflow_composer;
|
||||||
|
pub mod feedback_collector;
|
||||||
|
|
||||||
// Re-export main types for convenience
|
// Re-export main types for convenience
|
||||||
pub use types::{
|
pub use types::{
|
||||||
@@ -109,6 +111,11 @@ pub use pattern_aggregator::{AggregatedPattern, PatternAggregator};
|
|||||||
pub use skill_generator::{SkillCandidate, SkillGenerator};
|
pub use skill_generator::{SkillCandidate, SkillGenerator};
|
||||||
pub use quality_gate::{QualityGate, QualityReport};
|
pub use quality_gate::{QualityGate, QualityReport};
|
||||||
pub use evolution_engine::{EvolutionConfig, EvolutionEngine};
|
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
|
/// Growth system configuration
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
211
crates/zclaw-growth/src/workflow_composer.rs
Normal file
211
crates/zclaw-growth/src/workflow_composer.rs
Normal file
@@ -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<String>,
|
||||||
|
pub yaml_content: String,
|
||||||
|
pub source_sessions: Vec<String>,
|
||||||
|
pub confidence: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 工具链模式(用于聚类分析)
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
|
pub struct ToolChainPattern {
|
||||||
|
pub steps: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 工作流组装 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<String>)], // (session_id, tools_used)
|
||||||
|
) -> Vec<(ToolChainPattern, Vec<String>)> {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
let mut groups: HashMap<ToolChainPattern, Vec<String>> = 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<String>,
|
||||||
|
) -> Result<PipelineCandidate> {
|
||||||
|
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<String> = raw["triggers"]
|
||||||
|
.as_array()
|
||||||
|
.map(|a: &Vec<serde_json::Value>| {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -279,3 +279,4 @@ pub mod token_calibration;
|
|||||||
pub mod tool_error;
|
pub mod tool_error;
|
||||||
pub mod tool_output_guard;
|
pub mod tool_output_guard;
|
||||||
pub mod trajectory_recorder;
|
pub mod trajectory_recorder;
|
||||||
|
pub mod evolution;
|
||||||
|
|||||||
134
crates/zclaw-runtime/src/middleware/evolution.rs
Normal file
134
crates/zclaw-runtime/src/middleware/evolution.rs
Normal file
@@ -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<RwLock<Vec<PendingEvolution>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<PendingEvolution> {
|
||||||
|
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<MiddlewareDecision> {
|
||||||
|
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<evolution-suggestion>\n\
|
||||||
|
我注意到你经常做「{pattern}」相关的事情。\n\
|
||||||
|
我可以帮你整理成一个技能,以后直接说「{trigger}」就能用了。\n\
|
||||||
|
技能描述:{desc}\n\
|
||||||
|
如果你同意,请回复 '确认保存技能'。如果你想调整,可以告诉我怎么改。\n\
|
||||||
|
</evolution-suggestion>",
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user