fix(growth): Evolution Engine 审计修复 — 7项全部完成
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
HIGH-1: 提取共享 json_utils.rs,skill_generator/workflow_composer 去重
HIGH-2: FeedbackCollector Vec→HashMap,消除 unwrap() panic 风险
HIGH-3: ProfileUpdater 改为 collect_updates() 返回字段列表,
growth.rs 直接 async 调用 update_field(),不再用 no-op 闭包
MEDIUM-1: EvolutionMiddleware 注入后自动 drain,防止重复注入
MEDIUM-2: PatternAggregator tools 提取改为直接收集 context 值
MEDIUM-3: evolution_engine.rs 移除 4 个未使用 imports
MEDIUM-4: workflow_composer parse_response pattern 参数加下划线
MEDIUM-7: SkillCandidate 添加 version 字段(默认=1)
测试: zclaw-growth 128 tests, zclaw-runtime 86 tests, workspace 0 failures
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -9876,6 +9876,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wasmtime",
|
"wasmtime",
|
||||||
"wasmtime-wasi",
|
"wasmtime-wasi",
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::experience_store::ExperienceStore;
|
use crate::experience_store::ExperienceStore;
|
||||||
use crate::feedback_collector::{
|
use crate::feedback_collector::{
|
||||||
EvolutionArtifact, FeedbackCollector, FeedbackEntry, FeedbackSignal, RecommendedAction,
|
FeedbackCollector, FeedbackEntry, TrustUpdate,
|
||||||
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};
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
//! 收集用户对进化产物(技能/Pipeline)的显式/隐式反馈
|
//! 收集用户对进化产物(技能/Pipeline)的显式/隐式反馈
|
||||||
//! 管理信任度衰减和优化循环
|
//! 管理信任度衰减和优化循环
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -58,21 +60,32 @@ pub struct TrustRecord {
|
|||||||
|
|
||||||
/// 反馈收集器
|
/// 反馈收集器
|
||||||
/// 管理反馈记录和信任度评分
|
/// 管理反馈记录和信任度评分
|
||||||
|
/// 内存存储,可持久化到 SQLite(后续版本)
|
||||||
pub struct FeedbackCollector {
|
pub struct FeedbackCollector {
|
||||||
/// 信任度记录表(内存,可持久化到 SQLite)
|
trust_records: HashMap<String, TrustRecord>,
|
||||||
trust_records: Vec<TrustRecord>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeedbackCollector {
|
impl FeedbackCollector {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
trust_records: Vec::new(),
|
trust_records: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提交一条反馈
|
/// 提交一条反馈
|
||||||
pub fn submit_feedback(&mut self, entry: FeedbackEntry) -> TrustUpdate {
|
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;
|
record.total_feedback += 1;
|
||||||
@@ -106,13 +119,13 @@ impl FeedbackCollector {
|
|||||||
|
|
||||||
/// 获取信任度记录
|
/// 获取信任度记录
|
||||||
pub fn get_trust(&self, artifact_id: &str) -> Option<&TrustRecord> {
|
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)
|
/// 获取所有需要优化的产物(信任度 < 0.4)
|
||||||
pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> {
|
pub fn get_artifacts_needing_optimization(&self) -> Vec<&TrustRecord> {
|
||||||
self.trust_records
|
self.trust_records
|
||||||
.iter()
|
.values()
|
||||||
.filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2)
|
.filter(|r| r.trust_score < 0.4 && r.total_feedback >= 2)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -120,7 +133,7 @@ impl FeedbackCollector {
|
|||||||
/// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5)
|
/// 获取所有应该归档的产物(信任度 < 0.2 且反馈 >= 5)
|
||||||
pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> {
|
pub fn get_artifacts_to_archive(&self) -> Vec<&TrustRecord> {
|
||||||
self.trust_records
|
self.trust_records
|
||||||
.iter()
|
.values()
|
||||||
.filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5)
|
.filter(|r| r.trust_score < 0.2 && r.total_feedback >= 5)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -128,37 +141,11 @@ impl FeedbackCollector {
|
|||||||
/// 获取所有高信任产物(信任度 >= 0.8)
|
/// 获取所有高信任产物(信任度 >= 0.8)
|
||||||
pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> {
|
pub fn get_recommended_artifacts(&self) -> Vec<&TrustRecord> {
|
||||||
self.trust_records
|
self.trust_records
|
||||||
.iter()
|
.values()
|
||||||
.filter(|r| r.trust_score >= 0.8)
|
.filter(|r| r.trust_score >= 0.8)
|
||||||
.collect()
|
.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(
|
fn calculate_trust_internal(
|
||||||
positive: u32,
|
positive: u32,
|
||||||
negative: u32,
|
negative: u32,
|
||||||
@@ -268,7 +255,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_recommend_optimize() {
|
fn test_recommend_optimize() {
|
||||||
let mut collector = FeedbackCollector::new();
|
let mut collector = FeedbackCollector::new();
|
||||||
// 2 negative → trust < 0.4
|
|
||||||
collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
|
collector.submit_feedback(make_feedback("skill-1", Sentiment::Negative));
|
||||||
let update = 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);
|
assert_eq!(update.action, RecommendedAction::Optimize);
|
||||||
|
|||||||
63
crates/zclaw-growth/src/json_utils.rs
Normal file
63
crates/zclaw-growth/src/json_utils.rs
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ pub mod storage;
|
|||||||
pub mod retrieval;
|
pub mod retrieval;
|
||||||
pub mod summarizer;
|
pub mod summarizer;
|
||||||
pub mod experience_store;
|
pub mod experience_store;
|
||||||
|
pub mod json_utils;
|
||||||
pub mod experience_extractor;
|
pub mod experience_extractor;
|
||||||
pub mod profile_updater;
|
pub mod profile_updater;
|
||||||
pub mod pattern_aggregator;
|
pub mod pattern_aggregator;
|
||||||
@@ -106,7 +107,8 @@ pub use experience_store::{Experience, ExperienceStore};
|
|||||||
pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer};
|
pub use retrieval::{EmbeddingClient, MemoryCache, QueryAnalyzer, SemanticScorer};
|
||||||
pub use summarizer::SummaryLlmDriver;
|
pub use summarizer::SummaryLlmDriver;
|
||||||
pub use experience_extractor::ExperienceExtractor;
|
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 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};
|
||||||
|
|||||||
@@ -52,15 +52,12 @@ impl PatternAggregator {
|
|||||||
let total_reuse: u32 = experiences.iter().map(|e| e.reuse_count).sum();
|
let total_reuse: u32 = experiences.iter().map(|e| e.reuse_count).sum();
|
||||||
let common_steps = Self::find_common_steps(&experiences);
|
let common_steps = Self::find_common_steps(&experiences);
|
||||||
|
|
||||||
// 从 context 字段提取工具名
|
// 从 context 字段提取工具名(context 存储的是触发工具/来源标识)
|
||||||
|
// Experience 结构没有独立的 tools 字段,context 作为来源标识使用
|
||||||
let tools: Vec<String> = experiences
|
let tools: Vec<String> = experiences
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|e| {
|
.map(|e| e.context.clone())
|
||||||
e.context
|
.filter(|s| !s.is_empty())
|
||||||
.split(',')
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
})
|
|
||||||
.collect::<std::collections::HashSet<_>>()
|
.collect::<std::collections::HashSet<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
//! 用户画像增量更新器
|
//! 用户画像增量更新器
|
||||||
//! 从 CombinedExtraction 的 profile_signals 更新 UserProfileStore
|
//! 从 CombinedExtraction 的 profile_signals 提取需要更新的字段
|
||||||
//! 不额外调用 LLM,纯规则驱动
|
//! 不额外调用 LLM,纯规则驱动
|
||||||
|
|
||||||
use crate::types::CombinedExtraction;
|
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;
|
pub struct UserProfileUpdater;
|
||||||
|
|
||||||
impl UserProfileUpdater {
|
impl UserProfileUpdater {
|
||||||
@@ -13,31 +21,30 @@ impl UserProfileUpdater {
|
|||||||
Self
|
Self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从提取结果更新用户画像
|
/// 从提取结果中收集需要更新的画像字段
|
||||||
/// profile_store 通过闭包注入,避免 zclaw-growth 依赖 zclaw-memory
|
/// 返回 (field, value) 列表,由调用方负责实际的异步写入
|
||||||
pub async fn update<F>(
|
pub fn collect_updates(
|
||||||
&self,
|
&self,
|
||||||
user_id: &str,
|
|
||||||
extraction: &CombinedExtraction,
|
extraction: &CombinedExtraction,
|
||||||
update_fn: F,
|
) -> Vec<ProfileFieldUpdate> {
|
||||||
) -> zclaw_types::Result<()>
|
|
||||||
where
|
|
||||||
F: Fn(&str, &str, &str) -> zclaw_types::Result<()> + Send + Sync,
|
|
||||||
{
|
|
||||||
let signals = &extraction.profile_signals;
|
let signals = &extraction.profile_signals;
|
||||||
|
let mut updates = Vec::new();
|
||||||
|
|
||||||
if let Some(ref industry) = signals.industry {
|
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 {
|
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 使用单独的方法(有去重和容量限制)
|
updates
|
||||||
// 这些通过 GrowthIntegration 中的具体调用处理
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,60 +57,37 @@ impl Default for UserProfileUpdater {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn test_update_industry() {
|
fn test_collect_updates_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(())
|
|
||||||
};
|
|
||||||
let mut extraction = CombinedExtraction::default();
|
let mut extraction = CombinedExtraction::default();
|
||||||
extraction.profile_signals.industry = Some("healthcare".to_string());
|
extraction.profile_signals.industry = Some("healthcare".to_string());
|
||||||
|
|
||||||
let updater = UserProfileUpdater::new();
|
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!(updates.len(), 1);
|
||||||
assert_eq!(locked.len(), 1);
|
assert_eq!(updates[0].field, "industry");
|
||||||
assert_eq!(locked[0].1, "industry");
|
assert_eq!(updates[0].value, "healthcare");
|
||||||
assert_eq!(locked[0].2, "healthcare");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn test_update_no_signals() {
|
fn test_collect_updates_no_signals() {
|
||||||
let update_fn =
|
|
||||||
|_: &str, _: &str, _: &str| -> zclaw_types::Result<()> { Ok(()) };
|
|
||||||
let extraction = CombinedExtraction::default();
|
let extraction = CombinedExtraction::default();
|
||||||
let updater = UserProfileUpdater::new();
|
let updater = UserProfileUpdater::new();
|
||||||
updater.update("user1", &extraction, update_fn).await.unwrap();
|
let updates = updater.collect_updates(&extraction);
|
||||||
// No panic = pass
|
assert!(updates.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[test]
|
||||||
async fn test_update_multiple_signals() {
|
fn test_collect_updates_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(())
|
|
||||||
};
|
|
||||||
let mut extraction = CombinedExtraction::default();
|
let mut extraction = CombinedExtraction::default();
|
||||||
extraction.profile_signals.industry = Some("ecommerce".to_string());
|
extraction.profile_signals.industry = Some("ecommerce".to_string());
|
||||||
extraction.profile_signals.communication_style = Some("concise".to_string());
|
extraction.profile_signals.communication_style = Some("concise".to_string());
|
||||||
|
|
||||||
let updater = UserProfileUpdater::new();
|
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!(updates.len(), 2);
|
||||||
assert_eq!(locked.len(), 2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ mod tests {
|
|||||||
body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(),
|
body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(),
|
||||||
source_pattern: "报表生成".to_string(),
|
source_pattern: "报表生成".to_string(),
|
||||||
confidence: 0.85,
|
confidence: 0.85,
|
||||||
|
version: 1,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ pub struct SkillCandidate {
|
|||||||
pub body_markdown: String,
|
pub body_markdown: String,
|
||||||
pub source_pattern: String,
|
pub source_pattern: String,
|
||||||
pub confidence: f32,
|
pub confidence: f32,
|
||||||
|
/// 技能版本号,用于后续迭代追踪
|
||||||
|
pub version: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// LLM 驱动的技能生成 prompt
|
/// LLM 驱动的技能生成 prompt
|
||||||
@@ -59,8 +61,7 @@ impl SkillGenerator {
|
|||||||
|
|
||||||
/// 解析 LLM 返回的 JSON 为 SkillCandidate
|
/// 解析 LLM 返回的 JSON 为 SkillCandidate
|
||||||
pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result<SkillCandidate> {
|
pub fn parse_response(json_str: &str, pattern: &AggregatedPattern) -> Result<SkillCandidate> {
|
||||||
// 尝试提取 JSON 块(LLM 可能包裹在 ```json ... ``` 中)
|
let json_str = crate::json_utils::extract_json_block(json_str);
|
||||||
let json_str = extract_json_block(json_str);
|
|
||||||
|
|
||||||
let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
|
let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
|
||||||
zclaw_types::ZclawError::ConfigError(format!("Invalid skill JSON: {}", 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(),
|
body_markdown: raw["body_markdown"].as_str().unwrap_or("").to_string(),
|
||||||
source_pattern: pattern.pain_pattern.clone(),
|
source_pattern: pattern.pain_pattern.clone(),
|
||||||
confidence: raw["confidence"].as_f64().unwrap_or(0.5) as f32,
|
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 {
|
impl Default for SkillGenerator {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
@@ -194,12 +171,12 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_extract_json_block_with_markdown() {
|
fn test_extract_json_block_with_markdown() {
|
||||||
let text = "Here is the result:\n```json\n{\"key\": \"value\"}\n```\nDone.";
|
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]
|
#[test]
|
||||||
fn test_extract_json_block_bare() {
|
fn test_extract_json_block_bare() {
|
||||||
let text = "{\"key\": \"value\"}";
|
let text = "{\"key\": \"value\"}";
|
||||||
assert_eq!(extract_json_block(text), "{\"key\": \"value\"}");
|
assert_eq!(crate::json_utils::extract_json_block(text), "{\"key\": \"value\"}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,10 +90,10 @@ impl WorkflowComposer {
|
|||||||
/// 解析 LLM 返回的 JSON 为 PipelineCandidate
|
/// 解析 LLM 返回的 JSON 为 PipelineCandidate
|
||||||
pub fn parse_response(
|
pub fn parse_response(
|
||||||
json_str: &str,
|
json_str: &str,
|
||||||
pattern: &ToolChainPattern,
|
_pattern: &ToolChainPattern,
|
||||||
source_sessions: Vec<String>,
|
source_sessions: Vec<String>,
|
||||||
) -> Result<PipelineCandidate> {
|
) -> Result<PipelineCandidate> {
|
||||||
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| {
|
let raw: serde_json::Value = serde_json::from_str(&json_str).map_err(|e| {
|
||||||
zclaw_types::ZclawError::ConfigError(format!("Invalid pipeline JSON: {}", 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 {
|
impl Default for WorkflowComposer {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
|
|||||||
@@ -324,25 +324,21 @@ impl GrowthIntegration {
|
|||||||
|
|
||||||
// Update user profile from extraction signals (L1 enhancement)
|
// Update user profile from extraction signals (L1 enhancement)
|
||||||
if let Some(profile_store) = &self.profile_store {
|
if let Some(profile_store) = &self.profile_store {
|
||||||
let _store = profile_store.clone();
|
let updates = self
|
||||||
let user_id = agent_id.to_string();
|
|
||||||
if let Err(e) = self
|
|
||||||
.profile_updater
|
.profile_updater
|
||||||
.update(&user_id, &combined_extraction, move |uid, field, val| {
|
.collect_updates(&combined_extraction);
|
||||||
// Synchronous wrapper — the actual update_field is async,
|
let user_id = agent_id.to_string();
|
||||||
// but we're already in an async context so we handle it via a future
|
for update in updates {
|
||||||
// For now, log and let the store handle it
|
if let Err(e) = profile_store
|
||||||
tracing::debug!(
|
.update_field(&user_id, &update.field, &update.value)
|
||||||
"[GrowthIntegration] Profile update: {} {}={}",
|
.await
|
||||||
uid,
|
{
|
||||||
field,
|
tracing::warn!(
|
||||||
val
|
"[GrowthIntegration] Profile update failed for {}: {}",
|
||||||
|
update.field,
|
||||||
|
e
|
||||||
);
|
);
|
||||||
Ok(())
|
}
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
tracing::warn!("[GrowthIntegration] Profile update failed: {}", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,13 +68,17 @@ impl AgentMiddleware for EvolutionMiddleware {
|
|||||||
&self,
|
&self,
|
||||||
ctx: &mut MiddlewareContext,
|
ctx: &mut MiddlewareContext,
|
||||||
) -> Result<MiddlewareDecision> {
|
) -> Result<MiddlewareDecision> {
|
||||||
let pending = self.pending.read().await;
|
let to_inject = {
|
||||||
if pending.is_empty() {
|
let mut pending = self.pending.write().await;
|
||||||
return Ok(MiddlewareDecision::Continue);
|
if pending.is_empty() {
|
||||||
}
|
return Ok(MiddlewareDecision::Continue);
|
||||||
|
}
|
||||||
|
// 只取第一条(最近的)事件注入,避免信息过载
|
||||||
|
// drain 已注入的事件,防止重复注入
|
||||||
|
std::mem::take(&mut *pending)
|
||||||
|
};
|
||||||
|
|
||||||
// 只在第一条(最近的)事件上触发提示,避免信息过载
|
if let Some(evolution) = to_inject.into_iter().next() {
|
||||||
if let Some(evolution) = pending.first() {
|
|
||||||
let injection = format!(
|
let injection = format!(
|
||||||
"\n\n<evolution-suggestion>\n\
|
"\n\n<evolution-suggestion>\n\
|
||||||
我注意到你经常做「{pattern}」相关的事情。\n\
|
我注意到你经常做「{pattern}」相关的事情。\n\
|
||||||
|
|||||||
Reference in New Issue
Block a user