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 (记忆进化) 在 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<VikingAdapter>,
|
||||
feedback: FeedbackCollector,
|
||||
config: EvolutionConfig,
|
||||
}
|
||||
|
||||
@@ -44,17 +51,16 @@ impl EvolutionEngine {
|
||||
pub fn new(viking: Arc<VikingAdapter>) -> Self {
|
||||
Self {
|
||||
viking,
|
||||
feedback: FeedbackCollector::new(),
|
||||
config: EvolutionConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Backward-compatible constructor
|
||||
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 {
|
||||
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<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)]
|
||||
|
||||
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 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)]
|
||||
|
||||
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_output_guard;
|
||||
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