feat(growth,kernel,runtime): Embedding 接通 + 自学习自动化 — A线+B线 6 项实现
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
A线 Embedding 接通: - A1: MemoryRetriever.set_embedding_client() + GrowthIntegration.configure_embedding() + Kernel.set_embedding_client() + viking_configure_embedding 传播到 Kernel - A2: Skill 路由替换 new_tf_idf_only() 为 EmbeddingAdapter + LlmSkillFallback B线 自学习自动化: - B1: evolution_bridge.rs — candidate_to_manifest() (PromptOnly, disabled by default) - B2: Kernel::generate_and_register_skill() 全链路 (LLM→parse→QualityGate→manifest→persist) - B3: EvolutionMiddleware 双模式 (auto_mode 跳过注入, 留给 kernel 自动处理) - B4: QualityGate 加固 (body ≥100字符 + 必须含标题 + 置信度上限 1.0) 验证: 934 tests PASS, 0 failures
This commit is contained in:
@@ -17,6 +17,7 @@ zclaw-runtime = { workspace = true }
|
||||
zclaw-protocols = { workspace = true }
|
||||
zclaw-hands = { workspace = true }
|
||||
zclaw-skills = { workspace = true }
|
||||
zclaw-growth = { workspace = true }
|
||||
|
||||
tokio = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
113
crates/zclaw-kernel/src/kernel/evolution_bridge.rs
Normal file
113
crates/zclaw-kernel/src/kernel/evolution_bridge.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Evolution Bridge — connects growth crate's SkillCandidate to skills crate's SkillManifest
|
||||
//!
|
||||
//! The growth crate (zclaw-growth) generates SkillCandidate from conversation patterns.
|
||||
//! The skills crate (zclaw-skills) requires SkillManifest for disk persistence.
|
||||
//! This bridge lives in zclaw-kernel because it depends on both crates.
|
||||
|
||||
use zclaw_growth::skill_generator::SkillCandidate;
|
||||
use zclaw_skills::{SkillManifest, SkillMode};
|
||||
use zclaw_types::SkillId;
|
||||
|
||||
/// Convert a validated SkillCandidate into a SkillManifest ready for registration.
|
||||
///
|
||||
/// Safety invariants:
|
||||
/// - `mode` is always `PromptOnly` (auto-generated skills cannot execute code)
|
||||
/// - `enabled` is `false` (requires one explicit positive feedback to activate)
|
||||
/// - `body_markdown` becomes the SKILL.md body content (stored by serialize_skill_md)
|
||||
pub fn candidate_to_manifest(candidate: &SkillCandidate) -> SkillManifest {
|
||||
let slug = name_to_slug(&candidate.name);
|
||||
|
||||
SkillManifest {
|
||||
id: SkillId::new(format!("auto-{}", slug)),
|
||||
name: candidate.name.clone(),
|
||||
description: candidate.description.clone(),
|
||||
version: format!("{}", candidate.version),
|
||||
author: Some("zclaw-evolution".to_string()),
|
||||
mode: SkillMode::PromptOnly,
|
||||
capabilities: Vec::new(),
|
||||
input_schema: None,
|
||||
output_schema: None,
|
||||
tags: vec!["auto-generated".to_string()],
|
||||
category: None,
|
||||
triggers: candidate.triggers.clone(),
|
||||
tools: candidate.tools.clone(),
|
||||
enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a human-readable name to a URL-safe slug.
|
||||
fn name_to_slug(name: &str) -> String {
|
||||
let mut result = String::new();
|
||||
for c in name.trim().chars() {
|
||||
if c.is_ascii_alphanumeric() {
|
||||
result.push(c.to_ascii_lowercase());
|
||||
} else if c == ' ' || c == '-' || c == '_' {
|
||||
result.push('-');
|
||||
} else {
|
||||
// Chinese/unicode characters: use hex representation
|
||||
result.push_str(&format!("{:x}", c as u32));
|
||||
}
|
||||
}
|
||||
result.trim_matches('-').to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_candidate() -> SkillCandidate {
|
||||
SkillCandidate {
|
||||
name: "每日报表".to_string(),
|
||||
description: "生成每日报表".to_string(),
|
||||
triggers: vec!["报表".to_string(), "日报".to_string()],
|
||||
tools: vec!["researcher".to_string()],
|
||||
body_markdown: "# 每日报表\n步骤1\n步骤2".to_string(),
|
||||
source_pattern: "报表生成".to_string(),
|
||||
confidence: 0.85,
|
||||
version: 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_candidate_to_manifest() {
|
||||
let candidate = make_candidate();
|
||||
let manifest = candidate_to_manifest(&candidate);
|
||||
|
||||
assert!(manifest.id.as_str().starts_with("auto-"));
|
||||
assert_eq!(manifest.name, "每日报表");
|
||||
assert_eq!(manifest.description, "生成每日报表");
|
||||
assert_eq!(manifest.version, "1");
|
||||
assert_eq!(manifest.author.as_deref(), Some("zclaw-evolution"));
|
||||
assert_eq!(manifest.mode, SkillMode::PromptOnly);
|
||||
assert!(!manifest.enabled, "auto-generated skills must start disabled");
|
||||
assert_eq!(manifest.triggers, candidate.triggers);
|
||||
assert_eq!(manifest.tools, candidate.tools);
|
||||
assert!(manifest.tags.contains(&"auto-generated".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_to_slug_ascii() {
|
||||
assert_eq!(name_to_slug("Daily Report"), "daily-report");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_to_slug_chinese() {
|
||||
let slug = name_to_slug("每日报表");
|
||||
assert!(!slug.is_empty());
|
||||
assert!(!slug.contains(' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_generated_always_prompt_only() {
|
||||
let candidate = make_candidate();
|
||||
let manifest = candidate_to_manifest(&candidate);
|
||||
assert_eq!(manifest.mode, SkillMode::PromptOnly);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_auto_generated_starts_disabled() {
|
||||
let candidate = make_candidate();
|
||||
let manifest = candidate_to_manifest(&candidate);
|
||||
assert!(!manifest.enabled);
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ mod triggers;
|
||||
mod approvals;
|
||||
mod orchestration;
|
||||
mod a2a;
|
||||
mod evolution_bridge;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{broadcast, Mutex};
|
||||
@@ -55,6 +56,8 @@ pub struct Kernel {
|
||||
growth: std::sync::Mutex<Option<std::sync::Arc<zclaw_runtime::GrowthIntegration>>>,
|
||||
/// Optional LLM driver for memory extraction (set by Tauri desktop layer)
|
||||
extraction_driver: Option<Arc<dyn zclaw_runtime::LlmDriverForExtraction>>,
|
||||
/// Optional embedding client for semantic search (set by Tauri desktop layer)
|
||||
embedding_client: Option<Arc<dyn zclaw_runtime::EmbeddingClient>>,
|
||||
/// MCP tool adapters — shared with Tauri MCP manager, updated dynamically
|
||||
mcp_adapters: Arc<std::sync::RwLock<Vec<zclaw_protocols::McpToolAdapter>>>,
|
||||
/// Dynamic industry keyword configs — shared with Tauri frontend, loaded from SaaS
|
||||
@@ -166,6 +169,7 @@ impl Kernel {
|
||||
viking,
|
||||
growth: std::sync::Mutex::new(None),
|
||||
extraction_driver: None,
|
||||
embedding_client: None,
|
||||
mcp_adapters: Arc::new(std::sync::RwLock::new(Vec::new())),
|
||||
industry_keywords: Arc::new(tokio::sync::RwLock::new(Vec::new())),
|
||||
a2a_router,
|
||||
@@ -258,7 +262,17 @@ impl Kernel {
|
||||
}
|
||||
|
||||
// Build semantic router from the skill registry (75 SKILL.md loaded at boot)
|
||||
let semantic_router = SemanticSkillRouter::new_tf_idf_only(self.skills.clone());
|
||||
let semantic_router = if let Some(ref embed_client) = self.embedding_client {
|
||||
let adapter = crate::skill_router::EmbeddingAdapter::new(embed_client.clone());
|
||||
let mut router = SemanticSkillRouter::new(self.skills.clone(), Arc::new(adapter));
|
||||
if let Some(llm_fallback) = self.make_llm_skill_fallback() {
|
||||
router = router.with_llm_fallback(llm_fallback);
|
||||
}
|
||||
tracing::debug!("[Kernel] SemanticSkillRouter created with embedding support");
|
||||
router
|
||||
} else {
|
||||
SemanticSkillRouter::new_tf_idf_only(self.skills.clone())
|
||||
};
|
||||
let adapter = SemanticRouterAdapter::new(Arc::new(semantic_router));
|
||||
let mw = zclaw_runtime::middleware::butler_router::ButlerRouterMiddleware::with_router_and_shared_keywords(
|
||||
Box::new(adapter),
|
||||
@@ -286,6 +300,10 @@ impl Kernel {
|
||||
if let Some(ref driver) = self.extraction_driver {
|
||||
g = g.with_llm_driver(driver.clone());
|
||||
}
|
||||
// Propagate embedding client to memory retriever if configured
|
||||
if let Some(ref embed_client) = self.embedding_client {
|
||||
g.configure_embedding(embed_client.clone());
|
||||
}
|
||||
*cached = Some(std::sync::Arc::new(g));
|
||||
}
|
||||
cached.as_ref().expect("growth present").clone()
|
||||
@@ -475,6 +493,25 @@ impl Kernel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the embedding client for semantic search.
|
||||
///
|
||||
/// Propagates to both the skill router (ButlerRouter) and memory retrieval
|
||||
/// (GrowthIntegration). The next middleware chain creation will use the
|
||||
/// configured client for embedding-based similarity.
|
||||
pub fn set_embedding_client(&mut self, client: Arc<dyn zclaw_runtime::EmbeddingClient>) {
|
||||
tracing::info!("[Kernel] Embedding client configured for semantic search");
|
||||
self.embedding_client = Some(client);
|
||||
// Invalidate cached GrowthIntegration so next request builds with new embedding
|
||||
if let Ok(mut g) = self.growth.lock() {
|
||||
*g = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an LLM skill fallback using the kernel's LLM driver.
|
||||
fn make_llm_skill_fallback(&self) -> Option<Arc<dyn zclaw_skills::semantic_router::RuntimeLlmIntent>> {
|
||||
Some(Arc::new(crate::skill_router::LlmSkillFallback::new(self.driver.clone())))
|
||||
}
|
||||
|
||||
/// Get a reference to the shared MCP adapters list.
|
||||
///
|
||||
/// The Tauri MCP manager updates this list when services start/stop.
|
||||
|
||||
@@ -76,4 +76,77 @@ impl Kernel {
|
||||
}
|
||||
self.skills.execute(&zclaw_types::SkillId::new(id), &ctx, input).await
|
||||
}
|
||||
|
||||
/// Generate a skill from an aggregated pattern and register it.
|
||||
///
|
||||
/// Full pipeline:
|
||||
/// 1. Build LLM prompt from pattern
|
||||
/// 2. Call LLM to get JSON response
|
||||
/// 3. Parse response into SkillCandidate
|
||||
/// 4. Validate through QualityGate (threshold 0.85 for auto-mode)
|
||||
/// 5. Convert to SkillManifest (PromptOnly, disabled by default)
|
||||
/// 6. Persist to disk via SkillRegistry
|
||||
pub async fn generate_and_register_skill(
|
||||
&self,
|
||||
pattern: &zclaw_growth::pattern_aggregator::AggregatedPattern,
|
||||
) -> Result<String> {
|
||||
// 1. Build prompt
|
||||
let prompt = zclaw_growth::skill_generator::SkillGenerator::build_prompt(pattern);
|
||||
|
||||
// 2. Call LLM
|
||||
let request = zclaw_runtime::driver::CompletionRequest {
|
||||
model: self.driver.provider().to_string(),
|
||||
system: Some("你是技能设计专家,只返回 JSON 格式的技能定义。".to_string()),
|
||||
messages: vec![zclaw_types::Message::user(prompt)],
|
||||
max_tokens: Some(1024),
|
||||
temperature: Some(0.3),
|
||||
stream: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let response = self.driver.complete(request).await?;
|
||||
let text = response.content.iter()
|
||||
.filter_map(|block| match block {
|
||||
zclaw_runtime::driver::ContentBlock::Text { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
|
||||
// 3. Parse into SkillCandidate
|
||||
let candidate = zclaw_growth::skill_generator::SkillGenerator::parse_response(
|
||||
&text, pattern,
|
||||
)?;
|
||||
|
||||
// 4. Validate through QualityGate (higher threshold for auto-generation)
|
||||
let existing_triggers: Vec<String> = self.skills.list().await
|
||||
.into_iter()
|
||||
.flat_map(|m| m.triggers)
|
||||
.collect();
|
||||
let gate = zclaw_growth::quality_gate::QualityGate::new(0.85, existing_triggers);
|
||||
let report = gate.validate_skill(&candidate);
|
||||
if !report.passed {
|
||||
return Err(zclaw_types::ZclawError::ConfigError(format!(
|
||||
"QualityGate rejected: {}", report.issues.join("; ")
|
||||
)));
|
||||
}
|
||||
|
||||
// 5. Convert to SkillManifest (PromptOnly, disabled)
|
||||
let manifest = super::evolution_bridge::candidate_to_manifest(&candidate);
|
||||
let skill_id = manifest.id.to_string();
|
||||
|
||||
// 6. Persist to disk
|
||||
let skills_dir = self.config.skills_dir.as_ref()
|
||||
.ok_or_else(|| zclaw_types::ZclawError::InvalidInput(
|
||||
"Skills directory not configured".into()
|
||||
))?;
|
||||
self.skills.create_skill(skills_dir, manifest).await?;
|
||||
|
||||
tracing::info!(
|
||||
"[Kernel] Auto-generated skill '{}' (id={}) registered (disabled)",
|
||||
candidate.name, skill_id
|
||||
);
|
||||
|
||||
Ok(skill_id)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user